├── .coveralls.yml ├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .rubocop.yml ├── .solr_wrapper.yml ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ └── blacklight │ │ │ └── maps.svg │ ├── javascripts │ │ ├── blacklight-maps.js │ │ └── blacklight-maps │ │ │ └── blacklight-maps-browse.js │ └── stylesheets │ │ └── blacklight_maps │ │ ├── blacklight-maps.scss │ │ └── default.scss ├── helpers │ ├── blacklight │ │ └── blacklight_maps_helper_behavior.rb │ └── blacklight_maps_helper.rb └── views │ └── catalog │ ├── _document_maps.html.erb │ ├── _index_mapview.html.erb │ ├── _map_placename_search.html.erb │ ├── _map_spatial_search.html.erb │ ├── _show_maplet_default.html.erb │ └── map.html.erb ├── blacklight-maps.gemspec ├── config ├── locales │ ├── blacklight-maps-zh.yml │ ├── blacklight-maps.ar.yml │ ├── blacklight-maps.de.yml │ ├── blacklight-maps.en.yml │ ├── blacklight-maps.es.yml │ ├── blacklight-maps.fr.yml │ ├── blacklight-maps.hu.yml │ ├── blacklight-maps.it.yml │ ├── blacklight-maps.nl.yml │ ├── blacklight-maps.pt-BR.yml │ └── blacklight-maps.sq.yml └── routes.rb ├── docs ├── blacklight-maps_index-view.png ├── blacklight-maps_map-view.png ├── blacklight-maps_search-control.png └── blacklight-maps_show-view.png ├── lib ├── blacklight │ ├── maps.rb │ └── maps │ │ ├── controller.rb │ │ ├── engine.rb │ │ ├── export.rb │ │ ├── geometry.rb │ │ ├── maps_search_builder.rb │ │ ├── render_constraints_override.rb │ │ └── version.rb ├── generators │ └── blacklight_maps │ │ ├── install_generator.rb │ │ └── templates │ │ ├── blacklight_maps.css.scss │ │ ├── search_history_controller.rb │ │ └── solr │ │ └── conf │ │ ├── schema.xml │ │ └── solrconfig.xml └── railties │ └── blacklight_maps.rake ├── spec ├── controllers │ └── catalog_controller_spec.rb ├── fixtures │ └── sample_solr_documents.yml ├── helpers │ └── blacklight_maps_helper_spec.rb ├── lib │ └── blacklight │ │ └── maps │ │ ├── export_spec.rb │ │ ├── geometry_spec.rb │ │ ├── maps_search_builder_spec.rb │ │ └── render_constraints_override_spec.rb ├── spec_helper.rb ├── system │ ├── index_view_spec.rb │ ├── initial_view_spec.rb │ ├── map_view_spec.rb │ └── show_view_maplet_spec.rb └── test_app_templates │ └── lib │ └── generators │ └── test_app_generator.rb └── vendor └── assets ├── images ├── layers-2x.png ├── layers.png ├── marker-icon-2x.png ├── marker-icon.png └── marker-shadow.png ├── javascripts ├── leaflet.js.erb └── leaflet.markercluster.js └── stylesheets ├── MarkerCluster.Default.css ├── MarkerCluster.css └── leaflet.css /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | name: test (ruby ${{ matrix.ruby }} / rails ${{ matrix.rails_version }} / blacklight ${{ matrix.blacklight_version }} ${{ matrix.additional_name }}) 13 | strategy: 14 | matrix: 15 | ruby: ["3.2", "3.3", "3.4"] 16 | rails_version: ["7.1.5.1", "7.2.2"] 17 | blacklight_version: ["~> 7.0"] 18 | additional_engine_cart_rails_options: [""] 19 | additional_name: [""] 20 | env: 21 | RAILS_VERSION: ${{ matrix.rails_version }} 22 | BLACKLIGHT_VERSION: ${{ matrix.blacklight_version }} 23 | ENGINE_CART_RAILS_OPTIONS: "--skip-action-cable ${{ matrix.additional_engine_cart_rails_options }}" 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Ruby ${{ matrix.ruby }} 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby }} 30 | - name: Install dependencies with Rails ${{ matrix.rails_version }} 31 | run: bundle install 32 | - name: Run tests 33 | run: bundle exec rake 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | spec/internal 19 | jetty 20 | .DS_Store 21 | .idea/ 22 | .rakeTasks 23 | .internal_test_app 24 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-rspec 3 | - rubocop-rails 4 | 5 | AllCops: 6 | DisplayCopNames: true 7 | TargetRubyVersion: 3.4 8 | Exclude: 9 | - "lib/generators/blacklight_maps/templates/**/*" 10 | - "blacklight-maps.gemspec" 11 | 12 | # engine_cart block includes conditional, not duplication 13 | Bundler/DuplicatedGem: 14 | Exclude: 15 | - 'Gemfile' 16 | 17 | # engine_cart block is following default Rails order 18 | Bundler/OrderedGems: 19 | Exclude: 20 | - 'Gemfile' 21 | 22 | Layout/IndentationConsistency: 23 | EnforcedStyle: normal 24 | 25 | Metrics/AbcSize: 26 | Max: 20 27 | Exclude: 28 | - 'lib/blacklight/maps/maps_search_builder.rb' 29 | 30 | Metrics/BlockLength: 31 | Exclude: 32 | - "spec/**/*" 33 | 34 | Metrics/ClassLength: 35 | Exclude: 36 | - 'lib/blacklight/maps/export.rb' 37 | 38 | Layout/LineLength: 39 | Max: 200 40 | Exclude: 41 | - 'lib/blacklight/maps/engine.rb' 42 | - 'spec/**/*' 43 | 44 | Metrics/MethodLength: 45 | Max: 15 46 | 47 | Naming/HeredocDelimiterNaming: 48 | Enabled: false 49 | 50 | Naming/PredicateName: 51 | ForbiddenPrefixes: 52 | - is_ 53 | 54 | Rails: 55 | Enabled: true 56 | 57 | Rails/HelperInstanceVariable: 58 | Enabled: false 59 | 60 | Rails/OutputSafety: 61 | Enabled: false 62 | 63 | RSpec/AnyInstance: 64 | Exclude: 65 | - 'spec/system/initial_view_spec.rb' 66 | 67 | RSpec/BeforeAfterAll: 68 | Enabled: false 69 | 70 | RSpec/DescribeClass: 71 | Exclude: 72 | - 'spec/system/*' 73 | 74 | RSpec/SpecFilePathFormat: 75 | Exclude: 76 | - 'spec/lib/blacklight/maps/*' 77 | 78 | RSpec/MessageSpies: 79 | EnforcedStyle: receive 80 | 81 | RSpec/MultipleExpectations: 82 | Max: 4 83 | 84 | RSpec/MultipleMemoizedHelpers: 85 | Max: 10 86 | 87 | RSpec/NestedGroups: 88 | Max: 5 89 | 90 | RSpec/PredicateMatcher: 91 | Exclude: 92 | - 'spec/lib/blacklight/maps/render_constraints_override_spec.rb' 93 | 94 | # https://github.com/rubocop-hq/rubocop/issues/6439 95 | Style/AccessModifierDeclarations: 96 | Enabled: false 97 | 98 | Style/Documentation: 99 | Enabled: false 100 | 101 | Style/SignalException: 102 | Exclude: 103 | - 'spec/**/*' 104 | -------------------------------------------------------------------------------- /.solr_wrapper.yml: -------------------------------------------------------------------------------- 1 | # Place any default configuration for solr_wrapper here 2 | # you must first run 'rake engine_cart:generate' to create the test app 3 | # before running 'solr_wrapper' from project root 4 | # port: 8983 5 | collection: 6 | dir: ./.internal_test_app/solr/conf/ 7 | name: blacklight-core 8 | version: 9.6.1 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Blacklight-Maps 2 | 3 | ## To submit a patch or feature 4 | 5 | 1. Fork it ( http://github.com//blacklight-maps/fork ) 6 | 2. Create your feature branch (`git checkout -b my-new-feature`) 7 | 3. Make some changes (with [tests](https://github.com/projectblacklight/blacklight/wiki/testing), please) 8 | 4. Commit your changes (`git commit -am 'Add some feature'`) 9 | 5. Push to the branch (`git push origin my-new-feature`) 10 | 6. Create new Pull Request 11 | 12 | ## Style preferences 13 | 14 | From version 0.3.2 forward, Blacklight-Maps will be using the [AirBnb JavaScript Style guide](https://github.com/airbnb/javascript) for JavaScript. Ruby code uses the community [Ruby Style Guide](https://github.com/bbatsov/ruby-style-guide). -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :development, :test do 8 | gem 'coveralls_reborn', require: false 9 | end 10 | 11 | # BEGIN ENGINE_CART BLOCK 12 | # engine_cart: 2.6.0 13 | # engine_cart stanza: 2.5.0 14 | # the below comes from engine_cart, a gem used to test this Rails engine gem in the context of a Rails app. 15 | file = File.expand_path('Gemfile', ENV['ENGINE_CART_DESTINATION'] || ENV['RAILS_ROOT'] || File.expand_path('.internal_test_app', File.dirname(__FILE__))) 16 | if File.exist?(file) 17 | begin 18 | eval_gemfile file 19 | rescue Bundler::GemfileError => e 20 | Bundler.ui.warn '[EngineCart] Skipping Rails application dependencies:' 21 | Bundler.ui.warn e.message 22 | end 23 | else 24 | Bundler.ui.warn "[EngineCart] Unable to find test application dependencies in #{file}, using placeholder dependencies" 25 | 26 | if ENV['RAILS_VERSION'] 27 | if ENV['RAILS_VERSION'] == 'edge' 28 | gem 'rails', github: 'rails/rails' 29 | ENV['ENGINE_CART_RAILS_OPTIONS'] = '--edge --skip-turbolinks' 30 | else 31 | gem 'rails', ENV['RAILS_VERSION'] 32 | end 33 | 34 | case ENV['RAILS_VERSION'] 35 | when /^6.0/ 36 | gem 'sass-rails', '>= 6' 37 | gem 'webpacker', '~> 4.0' 38 | when /^5.[12]/ 39 | gem 'sass-rails', '~> 5.0' 40 | gem 'sprockets', '~> 3.7' 41 | gem 'thor', '~> 0.20' 42 | end 43 | end 44 | end 45 | # END ENGINE_CART BLOCK 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014 The Board of Trustees of the Leland Stanford Junior University. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blacklight::Maps 2 | 3 | ![CI](https://github.com/projectblacklight/blacklight-maps/workflows/CI/badge.svg) | [![Coverage Status](https://coveralls.io/repos/projectblacklight/blacklight-maps/badge.svg?branch=master)](https://coveralls.io/r/projectblacklight/blacklight-maps?branch=master) 4 | 5 | Provides map views for Blacklight for items with geospatial coordinate (latitude/longitude) metadata. 6 | 7 | Browse all records by 'Map' view: 8 | ![Screen shot](docs/blacklight-maps_map-view.png) 9 | 10 | Map results view for search results (coordinate data as facet): 11 | ![Screen shot](docs/blacklight-maps_index-view.png) 12 | 13 | Maplet widget in item detail view: 14 | ![Screen shot](docs/blacklight-maps_show-view.png) 15 | 16 | ## Installation 17 | 18 | (See [Blacklight Version Compatibility](#blacklight-compatibility) below to make sure you're using a version of the gem that works with the version of Blacklight you're using.) 19 | 20 | Add this line to your application's Gemfile: 21 | 22 | gem 'blacklight-maps' 23 | 24 | And then execute: 25 | 26 | $ bundle 27 | 28 | Or install it yourself as: 29 | 30 | $ gem install blacklight-maps 31 | 32 | Run Blacklight-Maps generator: 33 | 34 | $ rails g blacklight_maps:install 35 | 36 | ## Usage 37 | 38 | Blacklight-Maps integrates [Leaflet](http://leafletjs.com/) to add map view capabilities for items with geospatial data in their corresponding Solr record. 39 | 40 | In the map views, locations are represented as markers (or marker clusters, depending on the zoom level). Clicking on a marker opens a popup which (depending on config settings) displays the location name or coordinates, and provides a link to search for other items with the same location name/coordinates. 41 | 42 | Users can also run a search using the map bounds as coordinate parameters by clicking the ![search control](docs/blacklight-maps_search-control.png) search control in the map view. Any items with coordinates or bounding boxes that are contained within the current map window will be returned. 43 | 44 | In the catalog#map and catalog#index views, the geospatial data to populate the map comes from the facet component of the Solr response. Bounding boxes are represented as points corresponding to the center of the box. 45 | 46 | In the catalog#show view, the data simply comes from the main document. Points are represented as markers and bounding boxes are represented as polygons. Clicking on a polygon opens a popup that allows the user to search for any items intersecting the bounding box. 47 | 48 | ### Solr Requirements 49 | 50 | Blacklight-Maps requires that your Solr index include at least one (but preferably BOTH) of the following two types of fields: 51 | 52 | 1. A `location_rpt` field that contains coordinates or a bounding box. For more on `location_rpt` see [Solr help](https://cwiki.apache.org/confluence/display/solr/Spatial+Search). This field can be multivalued. 53 | 54 | ``` 55 | # coordinates: lon lat or lat,lon 56 | # bounding box: ENVELOPE(minX, maxX, maxY, minY) 57 | coordinates_srpt: 58 | - 78.96288 20.593684 59 | - 20.593684,78.96288 60 | - ENVELOPE(68.162386, 97.395555, 35.5044752, 6.7535159) 61 | ``` 62 | 63 | 2. An indexed, stored string field containing a properly-formatted [GeoJSON](http://geojson.org) feature object for a point or bounding box that includes the coordinates and (preferably) location name. This field can be multivalued. 64 | 65 | ``` 66 | # first example below is for coordinate point, second is for bounding box 67 | geojson_ssim: 68 | - {"type":"Feature","geometry":{"type":"Point","coordinates":[78.96288,20.593684]},"properties":{"placename":"India"}} 69 | - {"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[68.162386, 6.7535159], [97.395555, 6.7535159], [97.395555, 35.5044752], [68.162386, 35.5044752], [68.162386, 6.7535159]]]},"bbox":[68.162386, 6.7535159, 97.395555, 35.5044752]} 70 | ``` 71 | 72 | If you have #2 above and you want the popup search links to use the location name as a search parameter, you also need: 73 | 74 | 3. An indexed, stored text or string field containing location names. This field can be multivalued. 75 | 76 | ``` 77 | subject_geo_ssim: India 78 | ``` 79 | 80 | ##### Why so complicated? 81 | Blacklight-Maps can be used with either field type (#1 or #2), however to take advantage of the full feature set, it is preferred that both field types exist for each item with geospatial metadata. 82 | 83 | * The GeoJSON field (#2 above) provides reliable association of place names with coordinates, so the map marker popups can display the location name 84 | * The Location name field (#3 above) allows users to run meaningful searches for locations found on the map 85 | * The Coordinate field (#1 above) provides for the "Search" function on the map in the catalog#map and catalog#index views 86 | 87 | 88 | **Important:** If you are NOT using the geojson field (#2), you should create a `copyField` in your Solr schema.xml to copy the coordinates from the `location_rpt` field to a string field that is stored, indexed, and multivalued to allow for proper faceting of the coordinate values in the catalog#map and catalog#index views. 89 | 90 | ``` 91 | 92 | 93 | 94 | 95 | ``` 96 | 97 | Support for additional field types may be added in the future. 98 | 99 | ### Configuration 100 | 101 | #### Required 102 | Blacklight-Maps expects you to provide these configuration options: 103 | 104 | + `facet_mode` = the type of field containing the data to use to display locations on the map (values: `'geojson'` or `'coordinates'`) 105 | - if `'geojson'`: 106 | + `geojson_field` = the name of the Solr field containing the GeoJSON data 107 | + `placename_property` = the key in the GeoJSON properties hash representing the location name 108 | - if `'coordinates'` 109 | + `coordinates_facet_field` = the name of the Solr field containing coordinate data in string format (`` of `coordinates_field`) 110 | + `search_mode` = the type of search to run when clicking a link in the map popups (values: `'placename'` or `'coordinates'`) 111 | - if `'placename'`: 112 | + `placename_field` = the name of the Solr field containing the location names 113 | + `coordinates_field` = the name of the Solr `location_rpt` type field containing geospatial coordinate data 114 | 115 | In addition, you must add the geospatial facet field to the list of facet fields in `app/controllers/catalog_controller.rb`, for example: 116 | ```ruby 117 | config.add_facet_field 'geojson_ssim', :limit => -2, :label => 'Coordinates', :show => false 118 | ``` 119 | 120 | #### Optional 121 | 122 | - `show_initial_zoom` = the zoom level to be used in the catalog#show view map (zoom levels for catalog#map and catalog#index map views are computed automatically) 123 | - `maxzoom` = the maxZoom [property of the map](http://leafletjs.com/reference.html#map-maxzoom) 124 | - `tileurl` = a [tileLayer url](http://leafletjs.com/reference.html#tilelayer-l.tilelayer) to change the basemap 125 | - `mapattribution` = an [attribution string](http://leafletjs.com/reference.html#tilelayer-attribution) to describe the basemap layer 126 | - `spatial_query_dist` = the radial distance, in kilometers, to search from a supplied coordinate point in a spatial search. This corresponds to the `d` [Spatial Filter](https://cwiki.apache.org/confluence/display/solr/Spatial+Search) parameter in Solr. 127 | 128 | 129 | All of these options can easily be configured in `CatalogController.rb` in the `config` block. 130 | 131 | ```ruby 132 | ... 133 | configure_blacklight do |config| 134 | ## blacklight-maps configuration default values 135 | config.view.maps.geojson_field = "geojson" 136 | config.view.maps.placename_property = "placename" 137 | config.view.maps.coordinates_field = "coordinates" 138 | config.view.maps.search_mode = "placename" # or "coordinates" 139 | config.view.maps.spatial_query_dist = 0.5 140 | config.view.maps.placename_field = "placename_field" 141 | config.view.maps.coordinates_facet_field = "coordinates_facet_field" 142 | config.view.maps.facet_mode = "geojson" # or "coordinates" 143 | config.view.maps.tileurl = "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" 144 | config.view.maps.mapattribution = 'Map data © OpenStreetMap contributors, CC-BY-SA' 145 | config.view.maps.maxzoom = 18 146 | config.view.maps.show_initial_zoom = 5 147 | 148 | config.add_facet_field 'geojson', :limit => -2, :label => 'Coordinates', :show => false 149 | ... 150 | 151 | ``` 152 | 153 | ### Implementation 154 | 155 | The catalog#map and catalog#index map views are available by default. The "browse everything" Map view will be available in your app at `/map`, and in your app using routing helper `map_path`. 156 | 157 | However, the catalog#show maplet widget must be included manually, via one of two ways: 158 | 159 | 1. Include the catalog/show_maplet_default partial explicitly. This option gives you the most flexibility, as you can choose where the partial gets rendered. 160 | 161 | ```ruby 162 | <%= render partial: 'catalog/show_maplet_default' %> 163 | ``` 164 | 165 | 2. Add `:show_maplet` to the list of partials to be rendered automatically by Blacklight in `CatalogController.rb` in the `config` block. This option is less work up front, but it may be more difficult to customize how the maplet is integrated into the page layout. 166 | 167 | ``` 168 | ... 169 | configure_blacklight do |config| 170 | # add :show_maplet to the show partials array 171 | config.show.partials << :show_maplet 172 | ... 173 | ``` 174 | 175 | ### Customization 176 | 177 | The ```blacklight_map_tag``` helper takes an options hash as one of its arguments that can be used to provide customization options for the Leaflet map functionality via data attributes. (See ```app/views/catalog/index_map``` for an example.) The available options include: 178 | 179 | Option | Type | Default | Description 180 | ------ | ---- | ------- | ----------- 181 | `initialview` | Array | `null` | the initial extend of the map as a 2d Array (e.g. `[[minLat, minLng], [maxLat, maxLng]]`) 182 | `searchcontrol` | Boolean | `false` | display the search control on the map 183 | `catalogpath` | String | `'catalog'` | the search path for the search control 184 | `placenamefield` | String | `'placename_field'` | the name of the Solr field containing the location names 185 | `searchctrlcue` | String | `'Search for all items within the current map window'` | the hover text to display when the mouse hovers over the ![search control](docs/blacklight-maps_search-control.png) search control 186 | `searchresultsview` | String | `'list'` | the view type for the search results on the catalog#index page after the map ![search control](docs/blacklight-maps_search-control.png) search control is used 187 | `singlemarkermode` | Boolean | `true` | whether locations should be clustered 188 | `clustercount` | String | `'locations'` | whether clusters should display the location count or the number of hits (`'hits'` or `'locations'`) 189 | `maxzoom` | Integer | 18 | the maxZoom [property of the map](http://leafletjs.com/reference.html#map-maxzoom) 190 | `tileurl` | String | `'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'` | a [tileLayer url](http://leafletjs.com/reference.html#tilelayer-l.tilelayer) to change the basemap 191 | `mapattribution` | String | ``Map data © OpenStreetMap contributors, CC-BY-SA'` | an [attribution string](http://leafletjs.com/reference.html#tilelayer-attribution) to describe the basemap layer 192 | `nodata` | String | `'Sorry, there is no data for this location.'` | a message to display in the Leaflet popup when the "popup" member is not present in the properties hash in the GeoJSON Feature for a location. 193 | 194 | ### Blacklight Version Compatibility 195 | The table below indicates which versions of Blacklight Maps are compatible with which versions of Blacklight. 196 | 197 | Blacklight Maps version | works with Blacklight version 198 | ----------------------- | --------------------- 199 | 1.2.* | >= 7.35.0, < 8 200 | 1.1.* | >= 7.8.0, < 8 201 | 0.5.* | >= 6.1.0, < 7 202 | 0.4.* | >= 5.12.0, < 6.* 203 | <= 0.3.3 | >= 5.1, <= 5.11.2 204 | 205 | ## Contributing 206 | 207 | We encourage you to contribute to Blacklight-Maps. Please see the [contributing guide](/CONTRIBUTING.md) for more information on contributing to the project. 208 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'bundler/setup' 5 | rescue LoadError 6 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 7 | end 8 | 9 | require 'rdoc/task' 10 | RDoc::Task.new(:rdoc) do |rdoc| 11 | rdoc.rdoc_dir = 'rdoc' 12 | rdoc.title = 'BlacklightMaps' 13 | rdoc.options << '--line-numbers' 14 | rdoc.rdoc_files.include('README.rdoc') 15 | rdoc.rdoc_files.include('lib/**/*.rb') 16 | end 17 | 18 | Bundler::GemHelper.install_tasks 19 | 20 | Rake::Task.define_task(:environment) 21 | 22 | load 'lib/railties/blacklight_maps.rake' 23 | 24 | task default: :ci 25 | 26 | require 'engine_cart/rake_task' 27 | 28 | require 'solr_wrapper' 29 | 30 | require 'rspec/core/rake_task' 31 | RSpec::Core::RakeTask.new 32 | 33 | require 'rubocop/rake_task' 34 | RuboCop::RakeTask.new(:rubocop) 35 | 36 | desc 'Run test suite' 37 | task ci: [:rubocop, 'engine_cart:generate'] do 38 | SolrWrapper.wrap do |solr| 39 | solr.with_collection do 40 | within_test_app do 41 | system 'RAILS_ENV=test rake blacklight_maps:index:seed' 42 | end 43 | Rake::Task['spec'].invoke 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/assets/images/blacklight/maps.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/blacklight-maps.js: -------------------------------------------------------------------------------- 1 | //= require leaflet 2 | //= require leaflet.markercluster 3 | 4 | //= require_tree . 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/blacklight-maps/blacklight-maps-browse.js: -------------------------------------------------------------------------------- 1 | ;(function( $ ) { 2 | 3 | $.fn.blacklight_leaflet_map = function(geojson_docs, arg_opts) { 4 | var map, sidebar, markers, geoJsonLayer, currentLayer; 5 | 6 | // Configure default options and those passed via the constructor options 7 | var options = $.extend({ 8 | tileurl : 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 9 | mapattribution : 'Map data © OpenStreetMap contributors, CC-BY-SA', 10 | initialzoom: 2, 11 | singlemarkermode: true, 12 | searchcontrol: false, 13 | catalogpath: 'catalog', 14 | searchctrlcue: 'Search for all items within the current map window', 15 | placenamefield: 'placename_field', 16 | nodata: 'Sorry, there is no data for this location.', 17 | clustercount:'locations', 18 | searchresultsview: 'list' 19 | }, arg_opts ); 20 | 21 | // Extend options from data-attributes 22 | $.extend(options, this.data()); 23 | 24 | var mapped_items = '' + geojson_docs.features.length + '' + ' location' + (geojson_docs.features.length !== 1 ? 's' : '') + ' mapped'; 25 | 26 | var mapped_caveat = 'Only items with location data are shown below'; 27 | 28 | var sortAndPerPage = $('#sortAndPerPage'); 29 | 30 | var markers; 31 | 32 | // Update page links with number of mapped items, disable sort, per_page, pagination 33 | if (sortAndPerPage.length) { // catalog#index and #map view 34 | var page_links = sortAndPerPage.find('.page-links'); 35 | var result_count = page_links.find('.page-entries').find('strong').last().html(); 36 | page_links.html('' + result_count + ' items found' + mapped_items + mapped_caveat); 37 | sortAndPerPage.find('.dropdown-toggle').hide(); 38 | } else { // catalog#show view 39 | $(this).before(mapped_items); 40 | } 41 | 42 | // determine whether to use item location or result count in cluster icon display 43 | if (options.clustercount == 'hits') { 44 | var clusterIconFunction = function (cluster) { 45 | var markers = cluster.getAllChildMarkers(); 46 | var childCount = 0; 47 | for (var i = 0; i < markers.length; i++) { 48 | childCount += markers[i].feature.properties.hits; 49 | } 50 | var c = ' marker-cluster-'; 51 | if (childCount < 10) { 52 | c += 'small'; 53 | } else if (childCount < 100) { 54 | c += 'medium'; 55 | } else { 56 | c += 'large'; 57 | } 58 | return new L.divIcon({ html: '
' + childCount + '
', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) }); 59 | }; 60 | } else { 61 | var clusterIconFunction = this._defaultIconCreateFunction; 62 | } 63 | 64 | // Display the map 65 | this.each(function() { 66 | options.id = this.id; 67 | 68 | // Setup Leaflet map 69 | map = L.map(this.id, { 70 | center: [0, 0], 71 | }); 72 | 73 | L.tileLayer(options.tileurl, { 74 | attribution: options.mapattribution, 75 | maxZoom: options.maxzoom 76 | }).addTo(map); 77 | 78 | // Create a marker cluster object and set options 79 | markers = new L.MarkerClusterGroup({ 80 | singleMarkerMode: options.singlemarkermode, 81 | iconCreateFunction: clusterIconFunction 82 | }); 83 | 84 | geoJsonLayer = L.geoJson(geojson_docs, { 85 | onEachFeature: function(feature, layer){ 86 | if (feature.properties.popup) { 87 | layer.bindPopup(feature.properties.popup); 88 | } else { 89 | layer.bindPopup(options.nodata); 90 | } 91 | } 92 | }); 93 | 94 | // Add GeoJSON layer to marker cluster object 95 | markers.addLayer(geoJsonLayer); 96 | 97 | // Add markers to map 98 | map.addLayer(markers); 99 | 100 | // Fit bounds of map 101 | setMapBounds(map); 102 | 103 | // create overlay for search control hover 104 | var searchHoverLayer = L.rectangle([[0,0], [0,0]], { 105 | color: "#0033ff", 106 | weight: 5, 107 | opacity: 0.5, 108 | fill: true, 109 | fillColor: "#0033ff", 110 | fillOpacity: 0.2 111 | }); 112 | 113 | // create search control 114 | var searchControl = L.Control.extend({ 115 | 116 | options: { position: 'topleft' }, 117 | 118 | onAdd: function (map) { 119 | var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control'); 120 | this.link = L.DomUtil.create('a', 'leaflet-bar-part search-control', container); 121 | this.link.title = options.searchctrlcue; 122 | 123 | L.DomEvent.addListener(this.link, 'click', _search); 124 | 125 | L.DomEvent.addListener(this.link, 'mouseover', function () { 126 | searchHoverLayer.setBounds(map.getBounds()); 127 | map.addLayer(searchHoverLayer); 128 | }); 129 | 130 | L.DomEvent.addListener(this.link, 'mouseout', function () { 131 | map.removeLayer(searchHoverLayer); 132 | }); 133 | 134 | return container; 135 | } 136 | 137 | }); 138 | 139 | // add search control to map 140 | if (options.searchcontrol === true) { 141 | map.addControl(new searchControl()); 142 | } 143 | 144 | }); 145 | 146 | /** 147 | * Sets the view of the map, based off of the map bounds 148 | * options.initialzoom is invoked for catalog#show views (unless it would obscure features) 149 | */ 150 | function setMapBounds() { 151 | map.fitBounds(mapBounds(), { 152 | padding: [10, 10], 153 | maxZoom: options.maxzoom 154 | }); 155 | if ($('#document').length) { 156 | if (map.getZoom() > options.initialzoom) { 157 | map.setZoom(options.initialzoom) 158 | } 159 | } 160 | } 161 | 162 | /** 163 | * Returns the bounds of the map based off of initialview being set or gets 164 | * the bounds of the markers object 165 | */ 166 | function mapBounds() { 167 | if (options.initialview) { 168 | return options.initialview; 169 | } else { 170 | return markerBounds(); 171 | } 172 | } 173 | 174 | /** 175 | * Returns the bounds of markers, if there are not any return 176 | */ 177 | function markerBounds() { 178 | if (hasAnyFeatures()) { 179 | return markers.getBounds(); 180 | } else { 181 | return [[90, 180], [-90, -180]]; 182 | } 183 | } 184 | 185 | /** 186 | * Checks to see if there are any features in the markers MarkerClusterGroup 187 | */ 188 | function hasAnyFeatures() { 189 | var has_features = false; 190 | markers.eachLayer(function (layer) { 191 | if (!$.isEmptyObject(layer)) { 192 | has_features = true; 193 | } 194 | }); 195 | return has_features; 196 | } 197 | 198 | // remove stale params, add new params, and run a new search 199 | function _search() { 200 | var params = filterParams(['view', 'spatial_search_type', 'coordinates', 'f%5B' + options.placenamefield + '%5D%5B%5D']), 201 | bounds = map.getBounds().toBBoxString().split(',').map(function(coord) { 202 | if (parseFloat(coord) > 180) { 203 | coord = '180' 204 | } else if (parseFloat(coord) < -180) { 205 | coord = '-180' 206 | } 207 | return Math.round(parseFloat(coord) * 1000000) / 1000000; 208 | }), 209 | coordinate_params = '[' + bounds[1] + ',' + bounds[0] + ' TO ' + bounds[3] + ',' + bounds[2] + ']'; 210 | params.push('coordinates=' + encodeURIComponent(coordinate_params), 'spatial_search_type=bbox', 'view=' + options.searchresultsview); 211 | $(location).attr('href', options.catalogpath + '?' + params.join('&')); 212 | } 213 | 214 | // remove unwanted params 215 | function filterParams(filterList) { 216 | var querystring = window.location.search.substr(1), 217 | params = []; 218 | if (querystring !== "") { 219 | params = $.map(querystring.split('&'), function(value) { 220 | if ($.inArray(value.split('=')[0], filterList) > -1) { 221 | return null; 222 | } else { 223 | return value; 224 | } 225 | }); 226 | } 227 | return params; 228 | } 229 | 230 | }; 231 | 232 | }( jQuery )); 233 | -------------------------------------------------------------------------------- /app/assets/stylesheets/blacklight_maps/blacklight-maps.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* Master manifest file for engine, so local app can require 3 | * this one file, but get all our files -- and local app 4 | * require does not need to change if we change file list. 5 | */ 6 | 7 | @import 'default'; -------------------------------------------------------------------------------- /app/assets/stylesheets/blacklight_maps/default.scss: -------------------------------------------------------------------------------- 1 | @import 'leaflet'; 2 | @import 'MarkerCluster'; 3 | @import 'MarkerCluster.Default'; 4 | 5 | body.blacklight-catalog-map { 6 | 7 | #map_leader_text { 8 | margin-bottom: 10px; 9 | } 10 | 11 | .view-type { 12 | display:none; 13 | } 14 | 15 | } 16 | 17 | #sortAndPerPage { 18 | 19 | .page-links { 20 | 21 | .mapped-count { 22 | margin-left: 10px; 23 | color: dimgray; 24 | } 25 | 26 | .mapped-caveat { 27 | margin-left: 10px; 28 | font-size: 12px; 29 | color: darkgray; 30 | } 31 | 32 | } 33 | 34 | } 35 | 36 | #documents { 37 | 38 | #blacklight-index-map { 39 | height: 550px; 40 | margin: 10px 0; 41 | 42 | & ~ div.record-padding { 43 | 44 | nav.pagination { 45 | display: none; 46 | } 47 | 48 | } 49 | 50 | } 51 | 52 | } 53 | 54 | #document #blacklight-show-map { 55 | height: 300px; 56 | } 57 | 58 | .mapped-count .badge { 59 | vertical-align: text-bottom; 60 | } 61 | 62 | a.leaflet-bar-part.search-control { 63 | cursor: pointer; 64 | &:before { content: "\1F50D"; } 65 | } 66 | 67 | /* Portrait tablet to landscape and desktop */ 68 | @media (min-width: 768px) and (max-width: 991px) { 69 | 70 | #sortAndPerPage { 71 | 72 | .page_links { 73 | width: 75%; 74 | padding: 0 12px 0 0; 75 | 76 | .mapped-caveat { 77 | margin-left: 0; 78 | float: left; 79 | } 80 | 81 | } 82 | 83 | } 84 | 85 | } 86 | 87 | -------------------------------------------------------------------------------- /app/helpers/blacklight/blacklight_maps_helper_behavior.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module BlacklightMapsHelperBehavior 5 | # @param id [String] the html element id 6 | # @param tag_options [Hash] options to put on the tag 7 | def blacklight_map_tag(id, tag_options = {}, &block) 8 | maps_config = blacklight_config.view.maps 9 | default_data = { 10 | maxzoom: maps_config.maxzoom, 11 | tileurl: maps_config.tileurl, 12 | mapattribution: maps_config.mapattribution 13 | } 14 | options = { id: id, data: default_data }.deep_merge(tag_options) 15 | block_given? ? content_tag(:div, options, &block) : tag.div(**options) 16 | end 17 | 18 | # return the placename value to be used as a link 19 | # @param geojson_hash [Hash] 20 | def placename_value(geojson_hash) 21 | geojson_hash[:properties][blacklight_config.view.maps.placename_property.to_sym] 22 | end 23 | 24 | # create a link to a bbox spatial search 25 | # @param bbox [Array] 26 | def link_to_bbox_search(bbox) 27 | bbox_coords = bbox.map(&:to_s) 28 | bbox_search_coords = "[#{bbox_coords[1]},#{bbox_coords[0]} TO #{bbox_coords[3]},#{bbox_coords[2]}]" 29 | link_to(t('blacklight.maps.interactions.bbox_search'), 30 | search_catalog_path(spatial_search_type: 'bbox', 31 | coordinates: bbox_search_coords, 32 | view: default_document_index_view_type)) 33 | end 34 | 35 | # create a link to a location name facet value 36 | # @param field_value [String] Solr field value 37 | # @param field [String] Solr field name 38 | # @param display_value [String] value to display instead of field_value 39 | def link_to_placename_field(field_value, field, display_value = nil) 40 | new_params = if params[:f] && params[:f][field]&.include?(field_value) 41 | search_state.params 42 | else 43 | search_state.add_facet_params(field, field_value) 44 | end 45 | new_params[:view] = default_document_index_view_type 46 | new_params.except!(:id, :spatial_search_type, :coordinates, :controller, :action) 47 | link_to(display_value.presence || field_value, search_catalog_path(new_params)) 48 | end 49 | 50 | # create a link to a spatial search for a set of point coordinates 51 | # @param point_coords [Array] 52 | def link_to_point_search(point_coords) 53 | new_params = params.except(:controller, :action, :view, :id, :spatial_search_type, :coordinates) 54 | new_params[:spatial_search_type] = 'point' 55 | new_params[:coordinates] = "#{point_coords[1]},#{point_coords[0]}" 56 | new_params[:view] = default_document_index_view_type 57 | new_params.permit! 58 | link_to(t('blacklight.maps.interactions.point_search'), search_catalog_path(new_params)) 59 | end 60 | 61 | # render the location name for the Leaflet popup 62 | # @param geojson_hash [Hash] 63 | def render_placename_heading(geojson_hash) 64 | geojson_hash[:properties][blacklight_config.view.maps.placename_property.to_sym] 65 | end 66 | 67 | # render the map for #index and #map views 68 | def render_index_mapview 69 | maps_config = blacklight_config.view.maps 70 | map_facet_field = if maps_config.facet_mode == 'coordinates' 71 | maps_config.coordinates_facet_field 72 | else 73 | maps_config.geojson_field 74 | end 75 | map_facet_values = @response.aggregations[map_facet_field]&.items || [] 76 | render partial: 'catalog/index_mapview', 77 | locals: { geojson_features: serialize_geojson(map_facet_values) } 78 | end 79 | 80 | # determine the type of spatial search to use based on coordinates (bbox or point) 81 | # @param coords [Array] 82 | def render_spatial_search_link(coords) 83 | coords.length == 4 ? link_to_bbox_search(coords) : link_to_point_search(coords) 84 | end 85 | 86 | # pass the document or facet values to BlacklightMaps::GeojsonExport 87 | # @param documents [Array || SolrDocument] 88 | def serialize_geojson(documents, options = {}) 89 | export = BlacklightMaps::GeojsonExport.new(controller, 90 | action_name.to_sym, 91 | documents, 92 | options) 93 | export.to_geojson 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /app/helpers/blacklight_maps_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BlacklightMapsHelper 4 | include Blacklight::BlacklightMapsHelperBehavior 5 | end 6 | -------------------------------------------------------------------------------- /app/views/catalog/_document_maps.html.erb: -------------------------------------------------------------------------------- 1 | <% # container for all documents in map view -%> 2 |
3 | <%= render_index_mapview %> 4 |
5 | -------------------------------------------------------------------------------- /app/views/catalog/_index_mapview.html.erb: -------------------------------------------------------------------------------- 1 | <%= blacklight_map_tag('blacklight-index-map', 2 | {data:{searchcontrol: true, 3 | catalogpath: search_catalog_path, 4 | placenamefield: blacklight_config.view.maps.placename_field, 5 | clustercount:'hits', 6 | searchresultsview: default_document_index_view_type 7 | }}) %> 8 | <%= javascript_tag "$('#blacklight-index-map').blacklight_leaflet_map(#{geojson_features});" %> -------------------------------------------------------------------------------- /app/views/catalog/_map_placename_search.html.erb: -------------------------------------------------------------------------------- 1 | <% # content for the popup for a point feature - run a new search for this location -%> 2 |
3 | <%= render_placename_heading(geojson_hash) %> 4 | <%= content_tag(:small, pluralize(hits, t('blacklight.maps.interactions.item'))) if hits %> 5 |
6 | <%= link_to_placename_field(placename_value(geojson_hash), 7 | blacklight_config.view.maps.placename_field, 8 | t('blacklight.maps.interactions.placename_search')) %> -------------------------------------------------------------------------------- /app/views/catalog/_map_spatial_search.html.erb: -------------------------------------------------------------------------------- 1 | <% # content for the popup for a point or bbox feature - run a new coordinate search for this location -%> 2 |
3 | <%= coordinates.length == 2 ? coordinates.reverse : coordinates %> 4 | <%= content_tag(:small, pluralize(hits, t('blacklight.maps.interactions.item'))) if hits %> 5 |
6 | <%= render_spatial_search_link(coordinates) %> -------------------------------------------------------------------------------- /app/views/catalog/_show_maplet_default.html.erb: -------------------------------------------------------------------------------- 1 | <% # map for catalog#show view %> 2 | <% if @document[blacklight_config.view.maps.geojson_field.to_sym] || @document[blacklight_config.view.maps.coordinates_field.to_sym] %> 3 |
4 | <%= blacklight_map_tag('blacklight-show-map', 5 | {data:{initialzoom:blacklight_config.view.maps.show_initial_zoom, 6 | singlemarkermode:false}}) %> 7 | <%= javascript_tag "$('#blacklight-show-map').blacklight_leaflet_map(#{serialize_geojson(@document)});" %> 8 |
9 | <% end %> 10 | -------------------------------------------------------------------------------- /app/views/catalog/map.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t('blacklight.maps.title') %>

3 |
<%= t('blacklight.maps.leader_html') %>
4 | <%= render 'search_results' %> 5 |
6 | 7 | <%# have to put this at the end so it overrides 'catalog/search_results' %> 8 | <% @page_title = t('blacklight.maps.title', application_name: application_name) %> -------------------------------------------------------------------------------- /blacklight-maps.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'blacklight/maps/version' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = 'blacklight-maps' 9 | s.version = Blacklight::Maps::VERSION 10 | s.authors = ['Chris Beer', 'Jack Reed', 'Eben English'] 11 | s.email = %w[cabeer@stanford.edu pjreed@stanford.edu eenglish@bpl.org] 12 | s.summary = 'Maps for Blacklight' 13 | s.description = 'Blacklight plugin providing map views for records with geographic data.' 14 | s.homepage = 'https://github.com/projectblacklight/blacklight-maps' 15 | s.license = 'Apache-2.0' 16 | 17 | s.files = `git ls-files -z`.split("\x0") 18 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 19 | s.bindir = 'exe' 20 | s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) } 21 | s.require_paths = ['lib'] 22 | 23 | s.add_dependency 'blacklight', '>= 7.35.0', '< 8' 24 | s.add_dependency 'rails', '>= 7.1', '< 8' 25 | 26 | s.add_development_dependency 'capybara' 27 | s.add_development_dependency 'engine_cart', '~> 2.6' 28 | s.add_development_dependency 'rspec-rails', '~> 7.1' 29 | s.add_development_dependency 'rubocop', '~> 1.72.2' 30 | s.add_development_dependency 'rubocop-rspec', '~> 3.4' 31 | s.add_development_dependency "rubocop-rails", '~> 2.30' 32 | s.add_development_dependency 'selenium-webdriver', '~> 4.0' 33 | s.add_development_dependency 'solr_wrapper', '~> 4.1' 34 | end 35 | -------------------------------------------------------------------------------- /config/locales/blacklight-maps-zh.yml: -------------------------------------------------------------------------------- 1 | zh: 2 | blacklight: 3 | 4 | maps: 5 | interactions: 6 | bbox_search: '查看与此边界框相交的项目' 7 | placename_search: '查看此位置的物品' 8 | item: '资料' 9 | point_search: '查看此位置的物品' 10 | search_ctrl_cue: '在当前地图窗口中搜索所有项目' 11 | title: '地图' 12 | leader: '单击标记以从该位置搜索项目,或使用 🔍 按钮搜索当前地图窗口中的所有项目。' 13 | 14 | search: 15 | filters: 16 | coordinates: 17 | bbox: '边界框' 18 | point: '座标' 19 | view: 20 | maps: '地图' 21 | -------------------------------------------------------------------------------- /config/locales/blacklight-maps.ar.yml: -------------------------------------------------------------------------------- 1 | ar: 2 | blacklight: 3 | 4 | maps: 5 | interactions: 6 | bbox_search: 'عرض العناصر التي تتقاطع مع هذا المربع المحيط' 7 | placename_search: 'عرض العناصر من هذا الموقع' 8 | item: 'مادة' 9 | point_search: 'عرض العناصر من هذا الموقع' 10 | search_ctrl_cue: 'ابحث عن جميع العناصر داخل نافذة الخريطة الحالية' 11 | title: 'خريطة' 12 | leader_html: "انقر فوق علامة للبحث عن عناصر من هذا الموقع ، أو استخدم الزر للبحث عن جميع العناصر الموجودة في نافذة الخريطة الحالية." 13 | 14 | search: 15 | filters: 16 | coordinates: 17 | bbox: 'المربع المحيط' 18 | point: 'إحداثيات' 19 | view: 20 | maps: 'خريطة' 21 | 22 | -------------------------------------------------------------------------------- /config/locales/blacklight-maps.de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | blacklight: 3 | 4 | maps: 5 | interactions: 6 | bbox_search: 'Zeigen Sie Elemente an, die sich mit diesem Begrenzungsrahmen überschneiden' 7 | placename_search: 'Zeigen Sie Elemente von diesem Speicherort aus an' 8 | item: 'Artikel' 9 | point_search: 'Zeigen Sie Elemente von diesem Speicherort aus an' 10 | search_ctrl_cue: 'Suchen Sie nach allen Elementen im aktuellen Kartenfenster' 11 | title: 'Karte' 12 | leader_html: "Klicken Sie auf eine Markierung, um nach Elementen von diesem Ort aus zu suchen, oder verwenden Sie die 🔍 Schaltfläche, um nach allen Elementen im aktuellen Kartenfenster zu suchen." 13 | 14 | search: 15 | filters: 16 | coordinates: 17 | bbox: 'Begrenzungsrahmen' 18 | point: 'Koordinaten' 19 | view: 20 | maps: 'Karte' 21 | 22 | -------------------------------------------------------------------------------- /config/locales/blacklight-maps.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | blacklight: 3 | 4 | maps: 5 | interactions: 6 | bbox_search: 'View items that intersect with this bounding box' 7 | placename_search: 'View items from this location' 8 | item: 'item' 9 | point_search: 'View items from this location' 10 | search_ctrl_cue: 'Search for all items within the current map window' 11 | title: 'Map' 12 | leader_html: "Click on a marker to search for items from that location, or use the 🔍 button to search for all items within the current map window." 13 | 14 | search: 15 | filters: 16 | coordinates: 17 | bbox: 'Bounding Box' 18 | point: 'Coordinates' 19 | view: 20 | maps: 'Map' 21 | 22 | -------------------------------------------------------------------------------- /config/locales/blacklight-maps.es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | blacklight: 3 | 4 | maps: 5 | interactions: 6 | bbox_search: 'Ver elementos que se cruzan con este cuadro delimitador' 7 | placename_search: 'Ver elementos desde esta ubicación' 8 | item: 'articulo' 9 | point_search: 'Ver elementos desde esta ubicación' 10 | search_ctrl_cue: 'Buscar todos los elementos dentro de la ventana del mapa actual' 11 | title: 'Mapa' 12 | leader_html: "Haga clic en un marcador para buscar elementos desde esa ubicación, o use el 🔍 botón para buscar todos los elementos dentro de la ventana del mapa actual." 13 | 14 | search: 15 | filters: 16 | coordinates: 17 | bbox: 'Cuadro delimitador' 18 | point: 'Coordenadas' 19 | view: 20 | maps: 'Mapa' 21 | 22 | -------------------------------------------------------------------------------- /config/locales/blacklight-maps.fr.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | blacklight: 3 | 4 | maps: 5 | interactions: 6 | bbox_search: 'Afficher les éléments qui coupent avec ce cadre de sélection' 7 | placename_search: 'Afficher les éléments de cet emplacement' 8 | item: 'article' 9 | point_search: 'Afficher les éléments de cet emplacement' 10 | search_ctrl_cue: 'Rechercher tous les éléments dans la fenêtre de carte actuelle' 11 | title: 'Carte' 12 | leader: 'Cliquez sur un marqueur pour rechercher des éléments à partir de cet emplacement, ou utilisez le 🔍 pour rechercher tous les éléments dans la fenêtre de carte actuelle.' 13 | 14 | search: 15 | filters: 16 | coordinates: 17 | bbox: 'Boîte de délimitation' 18 | point: 'Coordonnés' 19 | view: 20 | maps: 'Carte' 21 | -------------------------------------------------------------------------------- /config/locales/blacklight-maps.hu.yml: -------------------------------------------------------------------------------- 1 | hu: 2 | blacklight: 3 | 4 | maps: 5 | interactions: 6 | bbox_search: 'Tekintse meg azokat az elemeket, amelyek keresztezik ezt a határolódobozt' 7 | placename_search: 'Tekintse meg az ezen a helyen található elemeket' 8 | item: 'tétel' 9 | point_search: 'Tekintse meg az ezen a helyen található elemeket' 10 | search_ctrl_cue: 'Az összes elem keresése az aktuális térkép ablakban' 11 | title: 'Térkép' 12 | leader_html: "Kattintson a jelölőre az adott helyről elemek kereséséhez, vagy használja a 🔍 gombot az összes elem kereséséhez az aktuális térkép ablakban." 13 | 14 | search: 15 | filters: 16 | coordinates: 17 | bbox: 'Határoló doboz' 18 | point: 'Koordináták' 19 | view: 20 | maps: 'Térkép' 21 | 22 | -------------------------------------------------------------------------------- /config/locales/blacklight-maps.it.yml: -------------------------------------------------------------------------------- 1 | it: 2 | blacklight: 3 | 4 | maps: 5 | interactions: 6 | bbox_search: 'Visualizza gli elementi che si intersecano con questo rettangolo di selezione' 7 | placename_search: 'Visualizza articoli da questa posizione' 8 | item: 'articolo' 9 | point_search: 'Visualizza articoli da questa posizione' 10 | search_ctrl_cue: 'Cerca tutti gli elementi nella finestra della mappa corrente' 11 | title: 'Mappa' 12 | leader: 'Fai clic su un marcatore per cercare elementi da quella posizione oppure usa 🔍 per cercare tutti gli elementi nella finestra della mappa corrente.' 13 | 14 | search: 15 | filters: 16 | coordinates: 17 | bbox: 'Rettangolo di selezione' 18 | point: 'Coordinate' 19 | view: 20 | maps: 'Mappa' 21 | -------------------------------------------------------------------------------- /config/locales/blacklight-maps.nl.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | blacklight: 3 | 4 | maps: 5 | interactions: 6 | bbox_search: 'Bekijk items die kruisen met dit selectiekader' 7 | placename_search: 'Bekijk items van deze locatie' 8 | item: 'item' 9 | point_search: 'Bekijk items van deze locatie' 10 | search_ctrl_cue: 'Zoek naar alle items in het huidige kaartvenster' 11 | title: 'Kaart' 12 | leader_html: "Klik op een markering om naar items op die locatie te zoeken of gebruik de 🔍 knop om te zoeken naar alle items in het huidige kaartvenster." 13 | 14 | search: 15 | filters: 16 | coordinates: 17 | bbox: 'Begrenzende doos' 18 | point: 'Coördinaten' 19 | view: 20 | maps: 'Kaart' 21 | 22 | -------------------------------------------------------------------------------- /config/locales/blacklight-maps.pt-BR.yml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | blacklight: 3 | 4 | maps: 5 | interactions: 6 | bbox_search: 'Exibir itens que se cruzam com esta caixa delimitadora' 7 | placename_search: 'Ver itens deste local' 8 | item: 'item' 9 | point_search: 'Ver itens deste local' 10 | search_ctrl_cue: 'Pesquise todos os itens na janela atual do mapa' 11 | title: 'Mapa' 12 | leader_html: "Clique em um marcador para procurar itens desse local ou use o 🔍 para procurar todos os itens na janela atual do mapa." 13 | 14 | search: 15 | filters: 16 | coordinates: 17 | bbox: 'Caixa delimitadora' 18 | point: 'Coordenadas' 19 | view: 20 | maps: 'Mapa' 21 | 22 | -------------------------------------------------------------------------------- /config/locales/blacklight-maps.sq.yml: -------------------------------------------------------------------------------- 1 | sq: 2 | blacklight: 3 | 4 | maps: 5 | interactions: 6 | bbox_search: 'Shikoni artikujt që kryqëzohen me këtë kuti kufizuese' 7 | placename_search: 'Shikoni artikujt nga ky vendndodhje' 8 | item: 'artikull' 9 | point_search: 'Shikoni artikujt nga ky vendndodhje' 10 | search_ctrl_cue: 'Kërkoni për të gjithë artikujt brenda dritares aktuale të hartës' 11 | title: 'Hartë' 12 | leader_html: "Klikoni në një shënues për të kërkuar artikuj nga ai vend, ose përdorni 🔍 butoni për të kërkuar të gjitha artikujt brenda dritares aktuale të hartës." 13 | 14 | search: 15 | filters: 16 | coordinates: 17 | bbox: 'Kuti kufizuese' 18 | point: 'Koordinon' 19 | view: 20 | maps: 'Hartë' 21 | 22 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | get 'map', to: 'catalog#map', as: 'map' 5 | end 6 | -------------------------------------------------------------------------------- /docs/blacklight-maps_index-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/blacklight-maps/f04d48715ce485e318e537215675966a3ceb65d0/docs/blacklight-maps_index-view.png -------------------------------------------------------------------------------- /docs/blacklight-maps_map-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/blacklight-maps/f04d48715ce485e318e537215675966a3ceb65d0/docs/blacklight-maps_map-view.png -------------------------------------------------------------------------------- /docs/blacklight-maps_search-control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/blacklight-maps/f04d48715ce485e318e537215675966a3ceb65d0/docs/blacklight-maps_search-control.png -------------------------------------------------------------------------------- /docs/blacklight-maps_show-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/blacklight-maps/f04d48715ce485e318e537215675966a3ceb65d0/docs/blacklight-maps_show-view.png -------------------------------------------------------------------------------- /lib/blacklight/maps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'blacklight/maps/version' 4 | 5 | module Blacklight 6 | module Maps 7 | require 'blacklight/maps/controller' 8 | require 'blacklight/maps/render_constraints_override' 9 | require 'blacklight/maps/engine' 10 | require 'blacklight/maps/export' 11 | require 'blacklight/maps/geometry' 12 | require 'blacklight/maps/maps_search_builder' 13 | 14 | # returns the full path to the blacklight plugin installation 15 | def self.root 16 | @root ||= File.expand_path(File.dirname(File.dirname(File.dirname(__FILE__)))) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/blacklight/maps/controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BlacklightMaps 4 | module Controller 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | helper BlacklightMaps::RenderConstraintsOverride 9 | end 10 | 11 | def map 12 | (@response, @document_list) = search_service.search_results 13 | params[:view] = 'maps' 14 | respond_to do |format| 15 | format.html 16 | end 17 | end 18 | 19 | ## 20 | # BlacklightMaps override: update to look for spatial query params 21 | # Check if any search parameters have been set 22 | # @return [Boolean] 23 | def has_search_parameters? 24 | params[:coordinates].present? || super 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/blacklight/maps/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'blacklight' 4 | 5 | module Blacklight 6 | module Maps 7 | class Engine < Rails::Engine 8 | # Set some default configurations 9 | initializer 'blacklight-maps.default_config' do |_app| 10 | Blacklight::Configuration.default_values[:view].maps.geojson_field = 'geojson_ssim' 11 | Blacklight::Configuration.default_values[:view].maps.placename_property = 'placename' 12 | Blacklight::Configuration.default_values[:view].maps.coordinates_field = 'coordinates_srpt' 13 | Blacklight::Configuration.default_values[:view].maps.search_mode = 'placename' # or 'coordinates' 14 | Blacklight::Configuration.default_values[:view].maps.spatial_query_dist = 0.5 15 | Blacklight::Configuration.default_values[:view].maps.placename_field = 'subject_geo_ssim' 16 | Blacklight::Configuration.default_values[:view].maps.coordinates_facet_field = 'coordinates_ssim' 17 | Blacklight::Configuration.default_values[:view].maps.facet_mode = 'geojson' # or 'coordinates' 18 | Blacklight::Configuration.default_values[:view].maps.tileurl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' 19 | Blacklight::Configuration.default_values[:view].maps.mapattribution = 'Map data © OpenStreetMap contributors, CC-BY-SA' 20 | Blacklight::Configuration.default_values[:view].maps.maxzoom = 18 21 | Blacklight::Configuration.default_values[:view].maps.show_initial_zoom = 5 22 | end 23 | 24 | # Add our helpers 25 | initializer 'blacklight-maps.helpers' do |_app| 26 | config.after_initialize do 27 | ActionView::Base.include BlacklightMapsHelper 28 | end 29 | end 30 | 31 | # This makes our rake tasks visible. 32 | rake_tasks do 33 | Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))) do 34 | Dir.glob(File.join('railties', '*.rake')).each do |railtie| 35 | load railtie 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/blacklight/maps/export.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BlacklightMaps 4 | # This class provides the ability to export a response document to GeoJSON. 5 | # The export is formated as a GeoJSON FeatureCollection, where the features 6 | # consist of an array of Point features. For more on the GeoJSON 7 | # specification see http://geojson.org/geojson-spec.html. 8 | # 9 | class GeojsonExport 10 | include BlacklightMaps 11 | 12 | # @param controller [CatalogController] 13 | # @param action [Symbol] the controller action 14 | # @param response_docs [Array || SolrDocument] either: 15 | # - index view, map view: an array of facet values 16 | # - show view: the document object 17 | # @param options [Hash] optional hash of configuration options 18 | def initialize(controller, action, response_docs, options = {}) 19 | @controller = controller 20 | @action = action 21 | @response_docs = response_docs 22 | @options = options 23 | @features = [] 24 | end 25 | 26 | # builds the GeoJSON FeatureCollection 27 | def to_geojson 28 | { type: 'FeatureCollection', features: build_geojson_features }.to_json 29 | end 30 | 31 | private 32 | 33 | def maps_config 34 | @controller.blacklight_config.view.maps 35 | end 36 | 37 | def geojson_field 38 | maps_config.geojson_field 39 | end 40 | 41 | def coordinates_field 42 | maps_config.coordinates_field 43 | end 44 | 45 | def build_geojson_features 46 | if @action == :index || @action == :map 47 | build_index_features 48 | elsif @action == :show 49 | build_show_features 50 | end 51 | @features 52 | end 53 | 54 | # build GeoJSON features array for index and map views 55 | def build_index_features 56 | @response_docs.each do |geofacet| 57 | @features << if maps_config.facet_mode == 'coordinates' 58 | build_feature_from_coords(geofacet.value, geofacet.hits) 59 | else 60 | build_feature_from_geojson(geofacet.value, geofacet.hits) 61 | end 62 | end 63 | end 64 | 65 | # build GeoJSON features array for show view 66 | def build_show_features 67 | doc = @response_docs 68 | return unless doc[geojson_field] || doc[coordinates_field] 69 | 70 | if doc[geojson_field] 71 | build_features_from_geojson(doc[geojson_field]) 72 | elsif doc[coordinates_field] 73 | build_features_from_coords(doc[coordinates_field]) 74 | end 75 | end 76 | 77 | def build_features_from_geojson(geojson_field_values) 78 | return unless geojson_field_values 79 | 80 | geojson_field_values.uniq.each do |loc| 81 | @features << build_feature_from_geojson(loc) 82 | end 83 | end 84 | 85 | def build_features_from_coords(coordinates_field_values) 86 | return unless coordinates_field_values 87 | 88 | coordinates_field_values.uniq.each do |coords| 89 | @features << build_feature_from_coords(coords) 90 | end 91 | end 92 | 93 | # build GeoJSON feature from incoming GeoJSON data 94 | # turn bboxes into points for index view so we don't get weird mix of boxes and markers 95 | # @param loc [Hash] 96 | # @param hits [Integer] 97 | # rubocop:disable Metrics/AbcSize 98 | def build_feature_from_geojson(loc, hits = nil) 99 | geojson = JSON.parse(loc).deep_symbolize_keys 100 | if @action != :show && geojson[:bbox] 101 | bbox = Geometry::BoundingBox.new(geojson[:bbox]) 102 | geojson[:geometry][:coordinates] = Geometry::Point.new(bbox.find_center).normalize_for_search 103 | geojson[:geometry][:type] = 'Point' 104 | geojson.delete(:bbox) 105 | end 106 | geojson[:properties] ||= {} 107 | geojson[:properties][:hits] = hits.to_i if hits 108 | geojson[:properties][:popup] = render_leaflet_popup_content(geojson, hits) 109 | geojson 110 | end 111 | # rubocop:enable Metrics/AbcSize 112 | 113 | # build GeoJSON feature from incoming raw coordinate data 114 | # turn bboxes into points for index view so we don't get weird mix of boxes and markers 115 | # @param coords [String] 116 | # @param hits [Integer] 117 | def build_feature_from_coords(coords, hits = nil) 118 | geojson = { type: 'Feature', properties: {} } 119 | if coords =~ /ENVELOPE/ # bbox 120 | geojson.merge!(build_bbox_feature_from_coords(coords)) 121 | elsif coords =~ /^-?\d*\.?\d*[ ,]-?\d*\.?\d*$/ # point 122 | geojson[:geometry] = build_point_geometry(coords) 123 | else 124 | Rails.logger.error("This coordinate format is not yet supported: '#{coords}'") 125 | end 126 | geojson[:properties] = { popup: render_leaflet_popup_content(geojson, hits) } if geojson[:geometry][:coordinates] 127 | geojson[:properties][:hits] = hits.to_i if hits 128 | geojson 129 | end 130 | 131 | # @param coords [String] 132 | def build_bbox_feature_from_coords(coords) 133 | geojson = { geometry: {} } 134 | bbox = Geometry::BoundingBox.from_wkt_envelope(coords) 135 | if @action != :show 136 | geojson[:geometry][:type] = 'Point' 137 | geojson[:geometry][:coordinates] = Geometry::Point.new(bbox.find_center).normalize_for_search 138 | else 139 | coords_array = bbox.to_a 140 | geojson[:bbox] = coords_array 141 | geojson[:geometry][:type] = 'Polygon' 142 | geojson[:geometry][:coordinates] = bbox.geojson_geometry_array 143 | end 144 | geojson 145 | end 146 | 147 | # @param coords [String] 148 | def build_point_geometry(coords) 149 | geometry = { type: 'Point' } 150 | coords_array = coords =~ /,/ ? coords.split(',').reverse : coords.split(' ') 151 | geometry[:coordinates] = coords_array.map(&:to_f) 152 | geometry 153 | end 154 | 155 | # Render to string the partial for each individual feature. 156 | # For placename searching, render catalog/map_placename_search partial, 157 | # pass the full geojson hash to the partial for easier local customization 158 | # For coordinate searches (or features with only coordinate data), 159 | # render catalog/map_coordinate_search partial 160 | # @param geojson [Hash] 161 | # @param hits [Integer] 162 | def render_leaflet_popup_content(geojson, hits = nil) 163 | if maps_config.search_mode == 'placename' && 164 | geojson[:properties][maps_config.placename_property.to_sym] 165 | partial = 'catalog/map_placename_search' 166 | locals = { geojson_hash: geojson, hits: hits } 167 | else 168 | partial = 'catalog/map_spatial_search' 169 | locals = { coordinates: geojson[:bbox].presence || geojson[:geometry][:coordinates], hits: hits } 170 | end 171 | @controller.render_to_string(partial: partial, locals: locals) 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /lib/blacklight/maps/geometry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BlacklightMaps 4 | # Parent class of geospatial objects used in BlacklightMaps 5 | class Geometry 6 | # This class contains Bounding Box objects and methods for interacting with 7 | # them. 8 | class BoundingBox 9 | # points is an array containing longitude and latitude values which 10 | # relate to the southwest and northeast points of a bounding box 11 | # [west, south, east, north] ([minX, minY, maxX, maxY]). 12 | def initialize(points) 13 | @west = points[0].to_f 14 | @south = points[1].to_f 15 | @east = points[2].to_f 16 | @north = points[3].to_f 17 | end 18 | 19 | def geojson_geometry_array 20 | [ 21 | [ 22 | [@west, @south], 23 | [@east, @south], 24 | [@east, @north], 25 | [@west, @north], 26 | [@west, @south] 27 | ] 28 | ] 29 | end 30 | 31 | # Returns an array [lng, lat] which is the centerpoint of a BoundingBox. 32 | def find_center 33 | center = [] 34 | center[0] = (@west + @east) / 2 35 | center[1] = (@south + @north) / 2 36 | center[0] -= 180 if @west > @east # handle bboxes that cross the dateline 37 | center 38 | end 39 | 40 | # Creates a new bounding box from from a string of points 41 | # "-100 -50 100 50" (south west north east) 42 | def self.from_lon_lat_string(points) 43 | new(points.split(' ')) 44 | end 45 | 46 | # Creates a new bounding box from from a Solr WKT Envelope string 47 | # "ENVELOPE(34.26, 35.89, 33.33, 29.47)" (minX, maxX, maxY, minY) 48 | def self.from_wkt_envelope(envelope) 49 | coords = envelope.gsub(/[[A-Z]()]/, '')&.split(', ') 50 | new([coords[0], coords[3], coords[1], coords[2]]) 51 | end 52 | 53 | def to_a 54 | [@west, @south, @east, @north] 55 | end 56 | end 57 | 58 | # This class contains Point objects and methods for working with them 59 | class Point 60 | # points is an array corresponding to the longitude and latitude values 61 | # [long, lat] 62 | def initialize(points) 63 | @long = points[0].to_f 64 | @lat = points[1].to_f 65 | end 66 | 67 | # returns a string that can be used as the value of solr_parameters[:pt] 68 | # normalizes any long values >180 or <-180 69 | def normalize_for_search 70 | @long -= 360 if @long > 180 71 | @long += 360 if @long < -180 72 | [@long, @lat] 73 | end 74 | 75 | # Creates a new point from from a coordinate string 76 | # "-50,100" (lat,long) 77 | def self.from_lat_lon_string(points) 78 | new(points.split(',').reverse) 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/blacklight/maps/maps_search_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BlacklightMaps 4 | module MapsSearchBuilderBehavior 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | self.default_processor_chain += [:add_spatial_search_to_solr] 9 | end 10 | 11 | # add spatial search params to solr 12 | def add_spatial_search_to_solr(solr_parameters = {}) 13 | if blacklight_params[:spatial_search_type] && blacklight_params[:coordinates] 14 | solr_parameters[:fq] ||= [] 15 | if blacklight_params[:spatial_search_type] == 'bbox' 16 | solr_parameters[:fq] << "#{blacklight_config.view.maps.coordinates_field}:#{blacklight_params[:coordinates]}" 17 | else 18 | solr_parameters[:fq] << "{!geofilt sfield=#{blacklight_config.view.maps.coordinates_field}}" 19 | solr_parameters[:pt] = blacklight_params[:coordinates] 20 | solr_parameters[:d] = blacklight_config.view.maps.spatial_query_dist 21 | end 22 | end 23 | solr_parameters 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/blacklight/maps/render_constraints_override.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Meant to be applied on top of Blacklight view helpers, to over-ride 4 | # certain methods from RenderConstraintsHelper (newish in BL), 5 | # to affect constraints rendering 6 | module BlacklightMaps 7 | module RenderConstraintsOverride 8 | # @param search_state [Blacklight::SearchState] 9 | # @return [Boolean] 10 | def has_spatial_parameters?(search_state) 11 | search_state.params[:coordinates].present? 12 | end 13 | 14 | # BlacklightMaps override: check for coordinate parameters 15 | # @param params_or_search_state [Blacklight::SearchState || ActionController::Parameters] 16 | # @return [Boolean] 17 | def query_has_constraints?(params_or_search_state = search_state) 18 | search_state = convert_to_search_state(params_or_search_state) 19 | has_spatial_parameters?(search_state) || super 20 | end 21 | 22 | # BlacklightMaps override: include render_spatial_query() in rendered constraints 23 | # @param localized_params [Hash] localized_params query parameters 24 | # @param local_search_state [Blacklight::SearchState] 25 | # @return [String] 26 | def render_constraints(localized_params = params, local_search_state = search_state) 27 | params_or_search_state = if localized_params != params 28 | localized_params 29 | else 30 | local_search_state 31 | end 32 | render_spatial_query(params_or_search_state) + super 33 | end 34 | 35 | # BlacklightMaps override: include render_search_to_s_coord() in rendered constraints 36 | # Simpler textual version of constraints, used on Search History page. 37 | # @param params [Hash] 38 | # @return [String] 39 | def render_search_to_s(params) 40 | render_search_to_s_coord(params) + super 41 | end 42 | 43 | ## 44 | # Render the search query constraint 45 | # @param params [Hash] 46 | # @return [String] 47 | def render_search_to_s_coord(params) 48 | return ''.html_safe if params[:coordinates].blank? 49 | 50 | render_search_to_s_element(spatial_constraint_label(params), 51 | render_filter_value(params[:coordinates])) 52 | end 53 | 54 | # Render the spatial query constraints 55 | # @param params_or_search_state [Blacklight::SearchState || ActionController::Parameters] 56 | # @return [String] 57 | def render_spatial_query(params_or_search_state = search_state) 58 | search_state = convert_to_search_state(params_or_search_state) 59 | 60 | # So simple don't need a view template, we can just do it here. 61 | return ''.html_safe if search_state.params[:coordinates].blank? 62 | 63 | render_constraint_element(spatial_constraint_label(search_state), 64 | search_state.params[:coordinates], 65 | classes: ['coordinates'], 66 | remove: remove_spatial_params(search_state)) # _params.except!(:coordinates, :spatial_search_type) 67 | end 68 | 69 | ## 70 | # 71 | # @param search_state [Blacklight::SearchState] 72 | # @return [String] 73 | # remove the spatial params from params 74 | def remove_spatial_params(search_state) 75 | search_action_path(search_state.params.dup.except!(:coordinates, :spatial_search_type)) 76 | end 77 | 78 | ## 79 | # render the label for the spatial constraint 80 | # @param params_or_search_state [Blacklight::SearchState || ActionController::Parameters] 81 | # @return [String] 82 | def spatial_constraint_label(params_or_search_state) 83 | search_params = if params_or_search_state.is_a?(Blacklight::SearchState) 84 | params_or_search_state.params 85 | else 86 | params_or_search_state 87 | end 88 | if search_params[:spatial_search_type] && search_params[:spatial_search_type] == 'bbox' 89 | t('blacklight.search.filters.coordinates.bbox') 90 | else 91 | t('blacklight.search.filters.coordinates.point') 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/blacklight/maps/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Blacklight 4 | module Maps 5 | VERSION = '1.2.0' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/blacklight_maps/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/generators' 4 | 5 | module BlacklightMaps 6 | class Install < Rails::Generators::Base 7 | source_root File.expand_path('templates', __dir__) 8 | 9 | desc 'Install Blacklight-Maps' 10 | 11 | def verify_blacklight_installed 12 | return if IO.read('app/controllers/application_controller.rb').include?('include Blacklight::Controller') 13 | 14 | say_status('info', 'BLACKLIGHT NOT INSTALLED; GENERATING BLACKLIGHT', :blue) 15 | generate 'blacklight:install' 16 | end 17 | 18 | def assets 19 | copy_file 'blacklight_maps.css.scss', 'app/assets/stylesheets/blacklight_maps.css.scss' 20 | return if IO.read('app/assets/javascripts/application.js').include?('blacklight-maps') 21 | 22 | marker = '//= require blacklight/blacklight' 23 | insert_into_file 'app/assets/javascripts/application.js', after: marker do 24 | "\n// Required by BlacklightMaps" \ 25 | "\n//= require blacklight-maps" 26 | end 27 | append_to_file 'config/initializers/assets.rb', 28 | "\nRails.application.config.assets.paths << Rails.root.join('vendor', 'assets', 'images')\n" 29 | end 30 | 31 | def inject_search_builder 32 | inject_into_file 'app/models/search_builder.rb', 33 | after: /include Blacklight::Solr::SearchBuilderBehavior.*$/ do 34 | "\n include BlacklightMaps::MapsSearchBuilderBehavior\n" 35 | end 36 | end 37 | 38 | def install_catalog_controller_mixin 39 | inject_into_file 'app/controllers/catalog_controller.rb', 40 | after: /include Blacklight::Catalog.*$/ do 41 | "\n include BlacklightMaps::Controller\n" 42 | end 43 | end 44 | 45 | def install_search_history_controller 46 | target_file = 'app/controllers/search_history_controller.rb' 47 | if File.exist?(target_file) 48 | inject_into_file target_file, 49 | after: /include Blacklight::SearchHistory/ do 50 | "\n helper BlacklightMaps::RenderConstraintsOverride\n" 51 | end 52 | else 53 | copy_file 'search_history_controller.rb', target_file 54 | end 55 | end 56 | 57 | # TODO: inject Solr configuration (if needed) 58 | def inject_solr_configuration 59 | target_file = 'solr/conf/schema.xml' 60 | return unless File.exist?(target_file) 61 | 62 | inject_into_file target_file, 63 | after: %r{} do 64 | "\n \n" 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/generators/blacklight_maps/templates/blacklight_maps.css.scss: -------------------------------------------------------------------------------- 1 | /* 2 | *= require blacklight_maps/blacklight-maps 3 | */ -------------------------------------------------------------------------------- /lib/generators/blacklight_maps/templates/search_history_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SearchHistoryController < ApplicationController 4 | include Blacklight::SearchHistory 5 | 6 | helper BlacklightMaps::RenderConstraintsOverride 7 | end -------------------------------------------------------------------------------- /lib/generators/blacklight_maps/templates/solr/conf/schema.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 47 | 48 | 49 | 59 | 60 | 61 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 92 | 93 | 96 | 97 | 98 | 99 | 100 | 101 | 111 | 112 | 113 | 114 | 115 | 116 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 170 | 171 | 172 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 211 | 212 | 213 | 214 | 215 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 233 | 234 | 235 | 236 | 239 | 243 | 248 | 249 | 250 | 251 | 254 | 255 | 256 | 257 | 258 | 259 | 264 | 265 | 266 | 267 | 270 | 271 | 272 | 273 | 274 | 286 | 287 | 288 | 289 | 292 | 296 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 334 | 335 | 336 | 337 | 338 | 340 | 341 | 342 | 343 | 344 | 345 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 375 | 376 | 380 | 381 | 382 | 385 | 386 | 389 | 390 | 391 | 392 | 403 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 450 | 451 | 452 | 463 | 464 | 465 | 466 | 467 | 468 | 472 | 473 | 474 | 479 | 480 | 481 | 482 | 483 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 591 | 592 | 593 | 594 | 595 | 598 | id 599 | 600 | 601 | text 602 | 603 | 604 | 605 | 606 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 673 | 674 | 678 | 683 | 684 | 685 | 686 | -------------------------------------------------------------------------------- /lib/generators/blacklight_maps/templates/solr/conf/solrconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 23 | 24 | LUCENE_40 25 | 28 | 29 | 30 | 31 | 32 | 33 | ${solr.blacklight-core.data.dir:} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | solrpingquery 49 | 50 | 51 | all 52 | 53 | 54 | 55 | 56 | 57 | solr 58 | 59 | 60 | 69 | 70 | 73 | 74 | dismax 75 | explicit 76 | 10 77 | 78 | *:* 79 | 2<-1 5<-2 6<90% 80 | 81 | 88 | 89 | 90 | title_unstem_search^100000 91 | subtitle_unstem_search^50000 92 | title_t^25000 93 | subtitle_t^10000 94 | title_addl_unstem_search^5000 95 | title_addl_t^2500 96 | title_added_entry_unstem_search^1500 97 | title_added_entry_t^1250 98 | subject_topic_unstem_search^1000 99 | subject_unstem_search^750 100 | subject_topic_facet^625 101 | subject_t^500 102 | author_unstem_search^250 103 | author_addl_unstem_search^250 104 | author_t^100 105 | author_addl_t^50 106 | subject_addl_unstem_search^250 107 | subject_addl_t^50 108 | title_series_unstem_search^25 109 | title_series_t^10 110 | isbn_t 111 | text 112 | 113 | 114 | title_unstem_search^1000000 115 | subtitle_unstem_search^500000 116 | title_t^250000 117 | subtitle_t^100000 118 | title_addl_unstem_search^50000 119 | title_addl_t^25000 120 | title_added_entry_unstem_search^15000 121 | title_added_entry_t^12500 122 | subject_topic_unstem_search^10000 123 | subject_unstem_search^7500 124 | subject_topic_facet^6250 125 | subject_t^5000 126 | author_unstem_search^2500 127 | author_addl_unstem_search^2500 128 | author_t^1000 129 | author_addl_t^500 130 | subject_addl_unstem_search^2500 131 | subject_addl_t^500 132 | title_series_unstem_search^250 133 | title_series_t^100 134 | text^10 135 | 136 | 137 | author_unstem_search^200 138 | author_addl_unstem_search^50 139 | author_t^20 140 | author_addl_t 141 | 142 | 143 | author_unstem_search^2000 144 | author_addl_unstem_search^500 145 | author_t^200 146 | author_addl_t^10 147 | 148 | 149 | title_unstem_search^50000 150 | subtitle_unstem_search^25000 151 | title_addl_unstem_search^10000 152 | title_t^5000 153 | subtitle_t^2500 154 | title_addl_t^100 155 | title_added_entry_unstem_search^50 156 | title_added_entry_t^10 157 | title_series_unstem_search^5 158 | title_series_t 159 | 160 | 161 | title_unstem_search^500000 162 | subtitle_unstem_search^250000 163 | title_addl_unstem_search^100000 164 | title_t^50000 165 | subtitle_t^25000 166 | title_addl_t^1000 167 | title_added_entry_unstem_search^500 168 | title_added_entry_t^100 169 | title_series_t^50 170 | title_series_unstem_search^10 171 | 172 | 173 | subject_topic_unstem_search^200 174 | subject_unstem_search^125 175 | subject_topic_facet^100 176 | subject_t^50 177 | subject_addl_unstem_search^10 178 | subject_addl_t 179 | 180 | 181 | subject_topic_unstem_search^2000 182 | subject_unstem_search^1250 183 | subject_t^1000 184 | subject_topic_facet^500 185 | subject_addl_unstem_search^100 186 | subject_addl_t^10 187 | 188 | 189 | 3 190 | 0.01 191 | 192 | 193 | 194 | id, 195 | score, 196 | author_display, 197 | author_vern_display, 198 | format, 199 | isbn_t, 200 | language_facet, 201 | lc_callnum_display, 202 | material_type_display, 203 | published_display, 204 | published_vern_display, 205 | pub_date, 206 | title_display, 207 | title_vern_display, 208 | subject_topic_facet, 209 | subject_geo_facet, 210 | subject_era_facet, 211 | subtitle_display, 212 | subtitle_vern_display, 213 | url_fulltext_display, 214 | url_suppl_display, 215 | placename_coords, 216 | place_bbox 217 | 218 | 219 | true 220 | 1 221 | 10 222 | format 223 | lc_1letter_facet 224 | lc_alpha_facet 225 | lc_b4cutter_facet 226 | language_facet 227 | pub_date 228 | subject_era_facet 229 | subject_geo_facet 230 | subject_topic_facet 231 | true 232 | default 233 | true 234 | true 235 | false 236 | 5 237 | 238 | 239 | 243 | 252 | 257 | 273 | 281 | 285 | 291 | 292 | spellcheck 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | all 301 | * 302 | 1 303 | {!raw f=id v=$id} 304 | 305 | 306 | 307 | 308 | 309 | 310 | lucene 311 | explicit 312 | score desc, pub_date_sort desc, title_sort asc 313 | text 314 | AND 315 | 1 316 | 317 | 318 | 1 319 | 3 320 | 0.01 321 | 322 | 323 | 324 | author_unstem_search^200 325 | author_addl_unstem_search^50 326 | author_t^20 327 | author_addl_t 328 | 329 | 330 | author_unstem_search^2000 331 | author_addl_unstem_search^500 332 | author_t^200 333 | author_addl_t^10 334 | 335 | 336 | 337 | 338 | title_unstem_search^50000 339 | subtitle_unstem_search^25000 340 | title_addl_unstem_search^10000 341 | title_t^5000 342 | subtitle_t^2500 343 | title_addl_t^100 344 | title_added_entry_unstem_search^50 345 | title_added_entry_t^10 346 | title_series_unstem_search^5 347 | title_series_t 348 | 349 | 350 | title_unstem_search^500000 351 | subtitle_unstem_search^250000 352 | title_addl_unstem_search^100000 353 | title_t^50000 354 | subtitle_t^25000 355 | title_addl_t^1000 356 | title_added_entry_unstem_search^500 357 | title_added_entry_t^100 358 | title_series_t^50 359 | title_series_unstem_search^10 360 | 361 | 362 | 363 | 364 | subject_topic_unstem_search^200 365 | subject_unstem_search^125 366 | subject_topic_facet^100 367 | subject_t^50 368 | subject_addl_unstem_search^10 369 | subject_addl_t 370 | 371 | 372 | subject_topic_unstem_search^2000 373 | subject_unstem_search^1250 374 | subject_t^1000 375 | subject_topic_facet^500 376 | subject_addl_unstem_search^100 377 | subject_addl_t^10 378 | 379 | 380 | 381 | isbn_t 382 | 383 | 384 | text 385 | text^10 386 | 387 | 388 | 389 | id, 390 | score, 391 | author_display, 392 | author_vern_display, 393 | format, 394 | isbn_t, 395 | language_facet, 396 | lc_callnum_display, 397 | material_type_display, 398 | published_display, 399 | published_vern_display, 400 | pub_date, 401 | title_display, 402 | title_vern_display, 403 | subject_topic_facet, 404 | subject_geo_facet, 405 | subject_era_facet, 406 | subtitle_display, 407 | subtitle_vern_display, 408 | url_fulltext_display, 409 | url_suppl_display, 410 | 411 | 412 | true 413 | 1 414 | 10 415 | format 416 | lc_1letter_facet 417 | lc_alpha_facet 418 | lc_b4cutter_facet 419 | language_facet 420 | pub_date 421 | subject_era_facet 422 | subject_geo_facet 423 | subject_topic_facet 424 | 425 | true 426 | subject 427 | true 428 | true 429 | false 430 | 5 431 | 432 | 433 | spellcheck 434 | 435 | 436 | 437 | 444 | 445 | 446 | textSpell 447 | 448 | 451 | 452 | 455 | 456 | default 457 | spell 458 | ./spell 459 | true 460 | 461 | 462 | author 463 | author_spell 464 | ./spell_author 465 | 0.7 466 | true 467 | 468 | 469 | subject 470 | subject_spell 471 | ./spell_subject 472 | 0.7 473 | true 474 | 475 | 476 | title 477 | title_spell 478 | ./spell_title 479 | 0.7 480 | true 481 | 482 | 483 | 484 | 494 | 495 | 502 | 510 | 511 | 512 | 521 | 522 | 523 | 524 | -------------------------------------------------------------------------------- /lib/railties/blacklight_maps.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :blacklight_maps do 4 | namespace :index do 5 | desc 'Put sample data into solr' 6 | task seed: [:environment] do 7 | require 'yaml' 8 | docs = YAML.safe_load(File.open(File.join(Blacklight::Maps.root, 9 | 'spec', 10 | 'fixtures', 11 | 'sample_solr_documents.yml'))) 12 | conn = Blacklight.default_index.connection 13 | conn.add docs 14 | conn.commit 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/controllers/catalog_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe CatalogController do 6 | render_views 7 | 8 | # test setting configuration defaults in Blacklight::Maps::Engine here 9 | describe 'maps config' do 10 | let(:maps_config) { described_class.blacklight_config.view.maps } 11 | 12 | it 'sets the defaults in blacklight_config' do 13 | %i[geojson_field placename_property coordinates_field search_mode spatial_query_dist 14 | placename_field coordinates_facet_field facet_mode tileurl mapattribution maxzoom 15 | show_initial_zoom].each do |config_method| 16 | expect(maps_config.send(config_method)).not_to be_blank 17 | end 18 | end 19 | end 20 | 21 | describe "GET 'map'" do 22 | before { get :map } 23 | 24 | it 'responds to the #map action' do 25 | expect(response.code).to eq '200' 26 | end 27 | 28 | it "renders the '/map' partial" do 29 | expect(response.body).to have_selector('#blacklight-index-map') 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/helpers/blacklight_maps_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe BlacklightMapsHelper do 6 | let(:query_term) { 'Tibet' } 7 | let(:mock_controller) { CatalogController.new } 8 | let(:blacklight_config) { Blacklight::Configuration.new } 9 | let(:maps_config) { blacklight_config.view.maps } 10 | let(:search_service) do 11 | Blacklight::SearchService.new(config: blacklight_config, user_params: { q: query_term }) 12 | end 13 | let(:response) { search_service.search_results[0] } 14 | let(:docs) { response.aggregations[maps_config.geojson_field].items } 15 | let(:coords) { [91.117212, 29.646923] } 16 | let(:geojson_hash) do 17 | { type: 'Feature', geometry: { type: 'Point', coordinates: coords }, properties: { placename: query_term } } 18 | end 19 | let(:bbox) { [78.3955448, 26.8548157, 99.116241, 36.4833345] } 20 | 21 | before do 22 | mock_controller.request = ActionDispatch::TestRequest.create 23 | allow(helper).to receive_messages(controller: mock_controller, action_name: 'index') 24 | allow(helper).to receive_messages(blacklight_config: blacklight_config) 25 | allow(helper).to receive_messages(blacklight_configuration_context: Blacklight::Configuration::Context.new(mock_controller)) 26 | allow(helper).to receive(:search_state).and_return Blacklight::SearchState.new({}, blacklight_config, mock_controller) 27 | blacklight_config.add_facet_field 'geojson_ssim', limit: -2, label: 'GeoJSON', show: false 28 | blacklight_config.add_facet_fields_to_solr_request! 29 | end 30 | 31 | describe 'blacklight_map_tag' do 32 | context 'with default values' do 33 | subject { helper.blacklight_map_tag('blacklight-map') } 34 | 35 | it { is_expected.to have_selector 'div#blacklight-map' } 36 | it { is_expected.to have_selector "div[data-maxzoom='#{maps_config.maxzoom}']" } 37 | it { is_expected.to have_selector "div[data-tileurl='#{maps_config.tileurl}']" } 38 | it { is_expected.to have_selector "div[data-mapattribution='#{maps_config.mapattribution}']" } 39 | end 40 | 41 | context 'with custom values' do 42 | subject { helper.blacklight_map_tag('blacklight-map', data: { maxzoom: 6, tileurl: 'http://example.com/', mapattribution: 'hello world' }) } 43 | 44 | it { is_expected.to have_selector "div[data-maxzoom='6'][data-tileurl='http://example.com/'][data-mapattribution='hello world']" } 45 | end 46 | 47 | context 'when a block is provided' do 48 | subject { helper.blacklight_map_tag('foo') { content_tag(:span, 'bar') } } 49 | 50 | it { is_expected.to have_selector('div > span', text: 'bar') } 51 | end 52 | end 53 | 54 | describe 'serialize_geojson' do 55 | it 'returns geojson of documents' do 56 | expect(helper.serialize_geojson(docs)).to include('{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point"') 57 | end 58 | end 59 | 60 | describe 'placename_value' do 61 | it 'returns the placename value' do 62 | expect(helper.placename_value(geojson_hash)).to eq(query_term) 63 | end 64 | end 65 | 66 | describe 'link_to_bbox_search' do 67 | it 'creates a spatial search link' do 68 | expect(helper.link_to_bbox_search(bbox)).to include('catalog?coordinates') 69 | expect(helper.link_to_bbox_search(bbox)).to include('spatial_search_type=bbox') 70 | end 71 | 72 | it 'includes the default_document_index_view_type in the params' do 73 | expect(helper.link_to_bbox_search(bbox)).to include('view=list') 74 | end 75 | end 76 | 77 | describe 'link_to_placename_field' do 78 | subject { helper.link_to_placename_field(query_term, maps_config.placename_field) } 79 | 80 | it { is_expected.to include("catalog?f%5B#{maps_config.placename_field}%5D%5B%5D=Tibet") } 81 | it { is_expected.to include('view=list') } 82 | 83 | it 'creates a link to the placename field using the display value' do 84 | expect(helper.link_to_placename_field(query_term, maps_config.placename_field, 'foo')).to include('">foo') 85 | end 86 | end 87 | 88 | describe 'link_to_point_search' do 89 | it 'creates a link to a coordinate point' do 90 | expect(helper.link_to_point_search(coords)).to include('catalog?coordinates') 91 | expect(helper.link_to_point_search(coords)).to include('spatial_search_type=point') 92 | end 93 | 94 | it 'includes the default_document_index_view_type in the params' do 95 | expect(helper.link_to_point_search(coords)).to include('view=list') 96 | end 97 | end 98 | 99 | describe 'render_placename_heading' do 100 | it 'returns the placename heading' do 101 | expect(helper.render_placename_heading(geojson_hash)).to eq(query_term) 102 | end 103 | end 104 | 105 | describe 'render_index_mapview' do 106 | before { helper.instance_variable_set(:@response, response) } 107 | 108 | it 'renders the "catalog/index_mapview" partial' do 109 | expect(helper.render_index_mapview).to include("$('#blacklight-index-map').blacklight_leaflet_map") 110 | end 111 | end 112 | 113 | describe 'render_spatial_search_link' do 114 | it 'returns link_to_bbox_search if bbox coordinates are passed' do 115 | expect(helper.render_spatial_search_link(bbox)).to include('spatial_search_type=bbox') 116 | end 117 | 118 | it 'returns link_to_point_search if point coordinates are passed' do 119 | expect(helper.render_spatial_search_link(coords)).to include('spatial_search_type=point') 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /spec/lib/blacklight/maps/export_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe BlacklightMaps::GeojsonExport do 6 | let(:controller) { CatalogController.new } 7 | let(:action) { :index } 8 | let(:response_docs) do 9 | YAML.safe_load(File.open(File.join(RSpec.configuration.fixture_paths.first, 'sample_solr_documents.yml'))) 10 | end 11 | let(:export) { described_class.new(controller, action, response_docs, foo: 'bar') } 12 | 13 | before { controller.request = ActionDispatch::TestRequest.create } 14 | 15 | it 'instantiates a GeojsonExport instance' do 16 | expect(export.class).to eq(::BlacklightMaps::GeojsonExport) 17 | end 18 | 19 | describe 'return config settings' do 20 | it 'returns maps config' do 21 | expect(export.send(:maps_config).class).to eq(::Blacklight::Configuration::ViewConfig) 22 | end 23 | 24 | it 'returns geojson_field' do 25 | expect(export.send(:geojson_field)).to eq('geojson_ssim') 26 | end 27 | 28 | it 'returns coordinates_field' do 29 | expect(export.send(:coordinates_field)).to eq('coordinates_srpt') 30 | end 31 | 32 | it 'creates an @options instance variable' do 33 | expect(export.instance_variable_get('@options')[:foo]).to eq('bar') 34 | end 35 | end 36 | 37 | describe 'build_feature_from_geojson' do 38 | describe 'point feature' do 39 | let(:geojson) { '{"type":"Feature","geometry":{"type":"Point","coordinates":[104.195397,35.86166]},"properties":{"placename":"China"}}' } 40 | let(:point_feature) { export.send(:build_feature_from_geojson, geojson, 1) } 41 | 42 | it 'has a hits property with the right value' do 43 | expect(point_feature[:properties][:hits]).to eq(1) 44 | end 45 | 46 | it 'has a popup property' do 47 | expect(point_feature[:properties]).to have_key(:popup) 48 | end 49 | end 50 | 51 | describe 'bbox feature' do 52 | describe 'catalog#index view' do 53 | let(:geojson) { '{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[68.162386, 6.7535159], [97.395555, 6.7535159], [97.395555, 35.5044752], [68.162386, 35.5044752], [68.162386, 6.7535159]]]},"bbox":[68.162386, 6.7535159, 97.395555, 35.5044752]}' } 54 | let(:bbox_feature) { export.send(:build_feature_from_geojson, geojson, 1) } 55 | 56 | it 'sets the center point as the coordinates' do 57 | expect(bbox_feature[:geometry][:coordinates]).to eq([82.7789705, 21.12899555]) 58 | end 59 | 60 | it "changes the geometry type to 'Point'" do 61 | expect(bbox_feature[:geometry][:type]).to eq('Point') 62 | expect(bbox_feature[:bbox]).to be_nil 63 | end 64 | end 65 | end 66 | end 67 | 68 | describe 'build_feature_from_coords' do 69 | describe 'point feature' do 70 | let(:point_feature) { export.send(:build_feature_from_coords, '35.86166,104.195397', 1) } 71 | 72 | it 'creates a GeoJSON feature hash' do 73 | expect(point_feature.class).to eq(Hash) 74 | expect(point_feature[:type]).to eq('Feature') 75 | end 76 | 77 | it 'has the right coordinates' do 78 | expect(point_feature[:geometry][:coordinates]).to eq([104.195397, 35.86166]) 79 | end 80 | 81 | it 'has a hits property with the right value' do 82 | expect(point_feature[:properties][:hits]).to eq(1) 83 | end 84 | 85 | it 'has a popup property' do 86 | expect(point_feature[:properties]).to have_key(:popup) 87 | end 88 | end 89 | 90 | describe 'bbox feature' do 91 | let(:basic_bbox) { 'ENVELOPE(68.162386, 97.395555, 35.5044752, 6.7535159)' } 92 | 93 | describe 'catalog#index view' do 94 | let(:bbox_feature) { export.send(:build_feature_from_coords, basic_bbox, 1) } 95 | 96 | it 'sets the center point as the coordinates' do 97 | expect(bbox_feature[:geometry][:type]).to eq('Point') 98 | expect(bbox_feature[:geometry][:coordinates]).to eq([82.7789705, 21.12899555]) 99 | end 100 | 101 | describe 'bounding box that crosses the dateline' do 102 | let(:bbox_feature) do 103 | export.send(:build_feature_from_coords, 104 | 'ENVELOPE(1.162386, -179.395555, 35.5044752, 6.7535159)', 1) 105 | end 106 | 107 | it 'sets a center point with a long value between -180 and 180' do 108 | expect(bbox_feature[:geometry][:coordinates]).to eq([90.88341550000001, 21.12899555]) 109 | end 110 | end 111 | end 112 | 113 | describe 'catalog#show view' do 114 | let(:action) { :show } 115 | let(:show_feature) { export.send(:build_feature_from_coords, basic_bbox, 1) } 116 | 117 | it 'converts the bbox string to a polygon coordinate array' do 118 | expect(show_feature[:geometry][:type]).to eq('Polygon') 119 | expect(show_feature[:geometry][:coordinates]).to eq( 120 | [[[68.162386, 6.7535159], [97.395555, 6.7535159], [97.395555, 35.5044752], [68.162386, 35.5044752], [68.162386, 6.7535159]]] 121 | ) 122 | end 123 | 124 | it 'sets the bbox member' do 125 | expect(show_feature[:bbox]).to eq([68.162386, 6.7535159, 97.395555, 35.5044752]) 126 | end 127 | end 128 | end 129 | end 130 | 131 | describe 'render_leaflet_popup_content' do 132 | describe 'placename_facet search_mode' do 133 | let(:placename_popup) do 134 | export.send(:render_leaflet_popup_content, 135 | { type: 'Feature', 136 | geometry: { type: 'Point', coordinates: [104.195397, 35.86166] }, 137 | properties: { placename: 'China', hits: 1 } }) 138 | end 139 | let(:spatial_popup) do 140 | export.send(:render_leaflet_popup_content, 141 | { type: 'Feature', 142 | geometry: { type: 'Point', coordinates: [104.195397, 35.86166] }, 143 | properties: { hits: 1 } }) 144 | end 145 | 146 | it 'renders the map_placename_search partial if the placename is present' do 147 | expect(placename_popup).to include('href="/catalog?f%5Bsubject_geo_ssim%5D%5B%5D=China') 148 | end 149 | 150 | it 'renders the map_spatial_search partial if the placename is not present' do 151 | expect(spatial_popup).to include('href="/catalog?coordinates=35.86166%2C104.195397&spatial_search_type=point') 152 | end 153 | end 154 | 155 | describe 'coordinates search_mode' do 156 | before do 157 | CatalogController.configure_blacklight do |config| 158 | config.view.maps.search_mode = 'coordinates' 159 | end 160 | end 161 | 162 | let(:spatial_popup) do 163 | export.send(:render_leaflet_popup_content, 164 | { type: 'Feature', 165 | geometry: { type: 'Point', coordinates: [104.195397, 35.86166] }, 166 | properties: { hits: 1 } }) 167 | end 168 | 169 | it 'renders the map_spatial_search partial' do 170 | expect(spatial_popup).to include('href="/catalog?coordinates=35.86166%2C104.195397&spatial_search_type=point') 171 | end 172 | end 173 | end 174 | 175 | describe 'build_geojson_features' do 176 | let(:geojson_features) do 177 | described_class.new(controller, :show, response_docs.first).send(:build_geojson_features) 178 | end 179 | 180 | it 'creates an array of system' do 181 | expect(geojson_features).not_to be_blank 182 | end 183 | end 184 | 185 | describe 'to_geojson' do 186 | let(:feature_collection) do 187 | described_class.new(controller, :show, response_docs.first).send(:to_geojson) 188 | end 189 | 190 | it 'renders feature collection as json' do 191 | expect(feature_collection).to include('{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point"') 192 | end 193 | end 194 | end 195 | -------------------------------------------------------------------------------- /spec/lib/blacklight/maps/geometry_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe BlacklightMaps::Geometry do 6 | describe BlacklightMaps::Geometry::BoundingBox do 7 | let(:bbox) { described_class.from_lon_lat_string('-100 -50 100 50') } 8 | let(:bbox_california) { described_class.from_wkt_envelope('ENVELOPE(-124, -114, 42, 32)') } 9 | let(:bbox_dateline) { described_class.from_lon_lat_string('165 30 -172 -20') } 10 | 11 | it 'instantiates Geometry::BoundingBox' do 12 | expect(bbox.class).to eq(described_class) 13 | end 14 | 15 | describe '#find_center' do 16 | it 'returns center of simple bounding box' do 17 | expect(bbox.find_center).to eq([0.0, 0.0]) 18 | end 19 | end 20 | 21 | describe '#to_a' do 22 | it 'returns the coordinates as an array' do 23 | expect(bbox.to_a).to eq([-100, -50, 100, 50]) 24 | end 25 | end 26 | 27 | describe '#geojson_geometry_array' do 28 | it 'returns the coordinates as a multi dimensional array' do 29 | expect(bbox.geojson_geometry_array).to eq( 30 | [[[-100, -50], [100, -50], [100, 50], [-100, 50], [-100, -50]]] 31 | ) 32 | end 33 | end 34 | 35 | it 'returns center of California bounding box' do 36 | expect(bbox_california.find_center).to eq([-119.0, 37.0]) 37 | end 38 | 39 | it 'returns correct dateline bounding box' do 40 | expect(bbox_dateline.find_center).to eq([-183.5, 5]) 41 | end 42 | end 43 | 44 | describe BlacklightMaps::Geometry::Point do 45 | let(:point) { described_class.from_lat_lon_string('20,120') } 46 | let(:unparseable_point) { described_class.from_lat_lon_string('35.86166,-184.195397') } 47 | 48 | it 'instantiates Geometry::Point' do 49 | expect(point.class).to eq(described_class) 50 | end 51 | 52 | it 'returns a Solr-parseable coordinate if @long is > 180 or < -180' do 53 | expect(unparseable_point.normalize_for_search).to eq([175.804603, 35.86166]) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/lib/blacklight/maps/maps_search_builder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe BlacklightMaps::MapsSearchBuilderBehavior do 6 | let(:blacklight_config) { CatalogController.blacklight_config.deep_copy } 7 | let(:user_params) { {} } 8 | let(:context) { CatalogController.new } 9 | let(:search_builder_class) do 10 | Class.new(Blacklight::SearchBuilder) do 11 | include Blacklight::Solr::SearchBuilderBehavior 12 | include BlacklightMaps::MapsSearchBuilderBehavior 13 | end 14 | end 15 | let(:search_builder) { search_builder_class.new(context) } 16 | 17 | before { allow(context).to receive(:blacklight_config).and_return(blacklight_config) } 18 | 19 | describe 'add_spatial_search_to_solr' do 20 | describe 'coordinate search' do 21 | let(:coordinate_search) do 22 | search_builder.with(coordinates: '35.86166,104.195397', spatial_search_type: 'point') 23 | end 24 | 25 | it 'returns a coordinate point spatial search if coordinates are given' do 26 | expect(coordinate_search[:fq].first).to include('geofilt') 27 | expect(coordinate_search[:pt]).to eq('35.86166,104.195397') 28 | end 29 | end 30 | 31 | describe 'bbox search' do 32 | let(:bbox_search) do 33 | search_builder.with(coordinates: '[6.7535159,68.162386 TO 35.5044752,97.395555]', 34 | spatial_search_type: 'bbox') 35 | end 36 | 37 | it 'returns a bbox spatial search if a bbox is given' do 38 | expect(bbox_search[:fq].first).to include(blacklight_config.view.maps.coordinates_field) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/lib/blacklight/maps/render_constraints_override_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe BlacklightMaps::RenderConstraintsOverride, type: :helper do 6 | let(:mock_controller) { CatalogController.new } 7 | let(:blacklight_config) { Blacklight::Configuration.new } 8 | let(:test_params) { { coordinates: '35.86166,104.195397', spatial_search_type: 'point' } } 9 | let(:test_search_state) do 10 | Blacklight::SearchState.new(test_params, blacklight_config, mock_controller) 11 | end 12 | 13 | describe 'has_search_parameters?' do 14 | before { mock_controller.params = test_params } 15 | 16 | it 'returns true if coordinate params are present' do 17 | expect(mock_controller.has_search_parameters?).to be_truthy 18 | end 19 | end 20 | 21 | describe 'has_spatial_parameters?' do 22 | it 'returns true if coordinate params are present' do 23 | expect(helper.has_spatial_parameters?(test_search_state)).to be_truthy 24 | end 25 | end 26 | 27 | describe 'query_has_constraints?' do 28 | it 'returns true if there are coordinate params' do 29 | expect(helper.query_has_constraints?(test_search_state)).to be_truthy 30 | end 31 | end 32 | 33 | describe 'spatial_constraint_label' do 34 | let(:bbox_params) { { spatial_search_type: 'bbox' } } 35 | 36 | it 'returns the point label' do 37 | expect(helper.spatial_constraint_label(test_search_state)).to eq(I18n.t('blacklight.search.filters.coordinates.point')) 38 | end 39 | 40 | it 'returns the bbox label' do 41 | expect(helper.spatial_constraint_label(bbox_params)).to eq(I18n.t('blacklight.search.filters.coordinates.bbox')) 42 | end 43 | end 44 | 45 | describe 'render spatial constraints' do 46 | describe 'render_spatial_query' do 47 | before do 48 | allow(helper).to receive_messages(search_action_path: search_catalog_path) 49 | end 50 | 51 | it 'renders the coordinates' do 52 | expect(helper.render_spatial_query(test_search_state)).to have_content(test_params[:coordinates]) 53 | end 54 | 55 | it 'removes the spatial params' do 56 | expect(helper.remove_spatial_params(test_search_state)).not_to have_content('spatial_search_type') 57 | end 58 | end 59 | 60 | describe 'render_search_to_s_coord' do 61 | it 'returns render_search_to_s_element when coordinates are present' do 62 | expect(helper).to receive(:render_search_to_s_element) 63 | expect(helper).to receive(:render_filter_value) 64 | helper.render_search_to_s_coord(test_params) 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # testing environent: 4 | ENV['RAILS_ENV'] ||= 'test' 5 | 6 | require 'simplecov' 7 | require 'coveralls' 8 | Coveralls.wear!('rails') 9 | 10 | SimpleCov.formatter = Coveralls::SimpleCov::Formatter 11 | SimpleCov.start do 12 | add_filter '/spec/' 13 | end 14 | 15 | # engine_cart: 16 | require 'bundler/setup' 17 | require 'engine_cart' 18 | EngineCart.load_application! 19 | 20 | require 'blacklight/maps' 21 | 22 | require 'rspec/rails' 23 | require 'capybara/rspec' 24 | require 'selenium-webdriver' 25 | 26 | RSpec.configure do |config| 27 | config.infer_spec_type_from_file_location! 28 | config.fixture_paths = ["#{Blacklight::Maps.root}/spec/fixtures"] 29 | 30 | config.before(:each, :js, type: :system) do 31 | driven_by :selenium, using: :headless_chrome, screen_size: [1024, 768] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/system/index_view_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'catalog#index map view', :js do 6 | before do 7 | CatalogController.blacklight_config = Blacklight::Configuration.new 8 | CatalogController.configure_blacklight do |config| 9 | # use geojson facet for blacklight-maps catalog#index map view specs 10 | config.add_facet_field 'geojson_ssim', limit: -2, label: 'GeoJSON', show: false 11 | config.add_facet_field 'subject_geo_ssim', label: 'Region' 12 | config.add_facet_fields_to_solr_request! 13 | end 14 | visit search_catalog_path q: 'korea', view: 'maps' 15 | end 16 | 17 | it 'displays map elements' do 18 | expect(page).to have_selector('#documents.map') 19 | expect(page).to have_selector('#blacklight-index-map') 20 | end 21 | 22 | it 'displays tile layer attribution' do 23 | expect(find('div.leaflet-control-container')).to have_content('OpenStreetMap contributors, CC-BY-SA') 24 | end 25 | 26 | describe '#sortAndPerPage' do 27 | it 'shows the mapped item count' do 28 | expect(page).to have_selector('.mapped-count .badge', text: '4') 29 | end 30 | 31 | it 'shows the mapped item caveat' do 32 | expect(page).to have_selector('.mapped-caveat') 33 | end 34 | 35 | # TODO: placeholder spec: #sortAndPerPage > .view-type > .view-type-group 36 | # shows active map icon. however, this spec doesn't work because 37 | # Blacklight::ConfigurationHelperBehavior#has_alternative_views? returns false, 38 | # so catalog/_view_type_group partial renders no content, can't figure out why 39 | it 'shows the map view icon' do 40 | pending("expect(page).to have_selector('.view-type-maps.active')") 41 | fail 42 | end 43 | end 44 | 45 | describe 'data attributes' do 46 | let(:maxzoom) { CatalogController.blacklight_config.view.maps.maxzoom } 47 | let(:tileurl) { CatalogController.blacklight_config.view.maps.tileurl } 48 | 49 | it 'has maxzoom value from config' do 50 | expect(page).to have_selector("#blacklight-index-map[data-maxzoom='#{maxzoom}']") 51 | end 52 | 53 | it 'has tileurl value from config' do 54 | expect(page).to have_selector("#blacklight-index-map[data-tileurl='#{tileurl}']") 55 | end 56 | end 57 | 58 | describe 'marker clusters' do 59 | before do 60 | 3.times do # zoom out to create cluster 61 | find('a.leaflet-control-zoom-out').click 62 | sleep(1) # give Leaflet time to combine clusters or spec can fail 63 | end 64 | end 65 | 66 | it 'has one marker cluster' do 67 | expect(page).to have_selector('div.marker-cluster', count: 1) 68 | end 69 | 70 | it 'shows the result count' do 71 | expect(find('div.marker-cluster')).to have_content(4) 72 | end 73 | 74 | describe 'click marker cluster' do 75 | before { find('div.marker-cluster').click } 76 | 77 | it 'splits into two marker clusters' do 78 | expect(page).to have_selector('div.marker-cluster', count: 2) 79 | end 80 | end 81 | end 82 | 83 | describe 'marker popups' do 84 | before do 85 | find('.marker-cluster', text: '1', match: :first).click 86 | end 87 | 88 | it 'shows a popup with correct content' do 89 | expect(page).to have_selector('div.leaflet-popup-content-wrapper') 90 | expect(page).to have_css('.geo_popup_heading', text: 'Seoul (Korea)') 91 | end 92 | 93 | describe 'click search link' do 94 | before { find('div.leaflet-popup-content a').click } 95 | 96 | it 'runs a new search' do 97 | expect(page).to have_selector('.constraint-value .filter-value', text: 'Seoul (Korea)') 98 | end 99 | 100 | it 'uses the default view type' do 101 | expect(current_url).to include('view=list') 102 | end 103 | end 104 | end 105 | 106 | describe 'map search control' do 107 | it 'has a search control' do 108 | expect(page).to have_selector('.leaflet-control .search-control') 109 | end 110 | 111 | describe 'search control hover' do 112 | before { find('.search-control').hover } 113 | 114 | it 'adds a border to the map' do 115 | expect(page).to have_selector('.leaflet-overlay-pane path') 116 | end 117 | end 118 | 119 | describe 'search control click' do 120 | before { find('.search-control').click } 121 | 122 | it 'runs a new search' do 123 | expect(page).to have_selector('.constraint.coordinates') 124 | expect(current_url).to include('view=list') 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /spec/system/initial_view_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'Initial map bounds view parameter', :js do 6 | before(:all) do 7 | CatalogController.configure_blacklight do |config| 8 | config.view.maps.facet_mode = 'coordinates' 9 | config.view.maps.coordinates_facet_field = 'coordinates_ssim' 10 | config.add_facet_field 'coordinates_ssim', limit: -2, label: 'Coordinates', show: false 11 | end 12 | end 13 | 14 | it 'defaults to zoom area of markers' do 15 | visit search_catalog_path f: { format: ['Book'] }, view: 'maps' 16 | expect(page).to have_selector('.leaflet-marker-icon.marker-cluster', count: 9) 17 | end 18 | 19 | describe 'with provided initialview' do 20 | let(:map_tag) { '
'.html_safe } 21 | 22 | it 'sets map to correct bounds when initialview provided' do 23 | allow_any_instance_of(Blacklight::BlacklightMapsHelperBehavior).to receive(:blacklight_map_tag).and_return(map_tag) 24 | visit search_catalog_path f: { format: ['Book'] }, view: 'maps' 25 | expect(page).not_to have_selector('.leaflet-marker-icon.marker-cluster') 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/system/map_view_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'catalog#map view', :js do 6 | before do 7 | CatalogController.blacklight_config = Blacklight::Configuration.new 8 | CatalogController.configure_blacklight do |config| 9 | # use coordinates_facet facet for blacklight-maps catalog#map view specs 10 | config.view.maps.facet_mode = 'coordinates' 11 | config.view.maps.coordinates_facet_field = 'coordinates_ssim' 12 | config.add_facet_field 'coordinates_ssim', limit: -2, label: 'Coordinates', show: false 13 | config.add_facet_fields_to_solr_request! 14 | end 15 | visit map_path 16 | end 17 | 18 | it 'displays map elements' do 19 | expect(page).to have_selector('#documents.map') 20 | expect(page).to have_selector('#blacklight-index-map') 21 | end 22 | 23 | it 'displays some markers' do 24 | expect(page).to have_selector('div.marker-cluster') 25 | end 26 | 27 | describe 'marker popups' do 28 | before do 29 | 2.times do # zoom out to create cluster 30 | find('a.leaflet-control-zoom-in').click 31 | sleep(1) # give Leaflet time to split clusters or spec can fail 32 | end 33 | find('.marker-cluster:first-child').click 34 | end 35 | 36 | it 'shows a popup with correct content' do 37 | expect(page).to have_selector('.leaflet-popup-content-wrapper') 38 | expect(page).to have_css('.geo_popup_heading', text: '[35.86166, 104.195397]') 39 | end 40 | 41 | describe 'click search link' do 42 | before { find('div.leaflet-popup-content a').click } 43 | 44 | it 'runs a new search' do 45 | expect(page).to have_selector('.constraint-value .filter-value', text: '35.86166,104.195397') 46 | expect(current_url).to include('view=list') 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/system/show_view_maplet_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'catalog#show view', :js do 6 | before(:all) do 7 | CatalogController.blacklight_config = Blacklight::Configuration.new 8 | CatalogController.configure_blacklight do |config| 9 | config.show.partials << :show_maplet # add maplet to show view partials 10 | end 11 | end 12 | 13 | describe 'item with point feature' do 14 | before { visit solr_document_path('00314247') } 15 | 16 | it 'displays the maplet' do 17 | expect(page).to have_selector('#blacklight-show-map-container') 18 | end 19 | 20 | it 'has a single marker icon' do 21 | expect(page).to have_selector('.leaflet-marker-icon', count: 1) 22 | end 23 | 24 | describe 'click marker icon' do 25 | before { find('.leaflet-marker-icon').click } 26 | 27 | it 'shows a popup with correct content' do 28 | expect(page).to have_selector('div.leaflet-popup-content-wrapper') 29 | expect(page).to have_content('Japan') 30 | end 31 | end 32 | end 33 | 34 | describe 'item with point and bbox system' do 35 | before { visit solr_document_path('2008308175') } 36 | 37 | it 'shows the correct mapped item count' do 38 | expect(page).to have_selector('.mapped-count .badge', text: '2') 39 | end 40 | 41 | it 'shows a bounding box and a point marker' do 42 | expect(page).to have_selector('.leaflet-overlay-pane path.leaflet-interactive') 43 | expect(page).to have_selector('.leaflet-marker-icon') 44 | end 45 | 46 | describe 'click bbox path' do 47 | before do 48 | 0.upto(4) { find('a.leaflet-control-zoom-in').click } # so bbox not covered by point 49 | find('.leaflet-overlay-pane svg').click 50 | end 51 | 52 | it 'shows a popup with correct content' do 53 | expect(page).to have_selector('div.leaflet-popup-content-wrapper') 54 | expect(page).to have_content('[68.162386, 6.7535159, 97.395555, 35.5044752]') 55 | end 56 | end 57 | end 58 | 59 | describe 'item with bbox feature' do 60 | before do 61 | CatalogController.configure_blacklight do |config| 62 | # set zoom config so we can test whether setMapBounds() is correct 63 | config.view.maps.maxzoom = 8 64 | config.view.maps.show_initial_zoom = 10 65 | end 66 | visit solr_document_path('2009373514') 67 | end 68 | 69 | it 'displays a bounding box' do 70 | expect(page).to have_selector('.leaflet-overlay-pane path.leaflet-interactive') 71 | end 72 | 73 | it 'zooms to the correct map bounds' do 74 | # if setMapBounds() zoom >= maxzoom, zoom-in control will be disabled 75 | expect(page).to have_selector('a.leaflet-control-zoom-in.leaflet-disabled') 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/test_app_templates/lib/generators/test_app_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/generators' 4 | 5 | class TestAppGenerator < Rails::Generators::Base 6 | source_root './spec/test_app_templates' 7 | 8 | def install_engine 9 | generate 'blacklight_maps:install' 10 | end 11 | 12 | def configure_test_assets 13 | insert_into_file 'config/environments/test.rb', after: 'Rails.application.configure do' do 14 | "\nconfig.assets.digest = false" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /vendor/assets/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/blacklight-maps/f04d48715ce485e318e537215675966a3ceb65d0/vendor/assets/images/layers-2x.png -------------------------------------------------------------------------------- /vendor/assets/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/blacklight-maps/f04d48715ce485e318e537215675966a3ceb65d0/vendor/assets/images/layers.png -------------------------------------------------------------------------------- /vendor/assets/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/blacklight-maps/f04d48715ce485e318e537215675966a3ceb65d0/vendor/assets/images/marker-icon-2x.png -------------------------------------------------------------------------------- /vendor/assets/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/blacklight-maps/f04d48715ce485e318e537215675966a3ceb65d0/vendor/assets/images/marker-icon.png -------------------------------------------------------------------------------- /vendor/assets/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectblacklight/blacklight-maps/f04d48715ce485e318e537215675966a3ceb65d0/vendor/assets/images/marker-shadow.png -------------------------------------------------------------------------------- /vendor/assets/javascripts/leaflet.markercluster.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e.Leaflet=e.Leaflet||{},e.Leaflet.markercluster=e.Leaflet.markercluster||{}))}(this,function(e){"use strict";var t=L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,clusterPane:L.Marker.prototype.options.pane,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animate:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,spiderLegPolylineOptions:{weight:1.5,color:"#222",opacity:.5},chunkedLoading:!1,chunkInterval:200,chunkDelay:50,chunkProgress:null,polygonOptions:{}},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.addEventParent(this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.addEventParent(this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null,this._queue=[],this._childMarkerEventHandlers={dragstart:this._childMarkerDragStart,move:this._childMarkerMoved,dragend:this._childMarkerDragEnd};var t=L.DomUtil.TRANSITION&&this.options.animate;L.extend(this,t?this._withAnimation:this._noAnimation),this._markerCluster=t?L.MarkerCluster:L.MarkerClusterNonAnimated},addLayer:function(e){if(e instanceof L.LayerGroup)return this.addLayers([e]);if(!e.getLatLng)return this._nonPointGroup.addLayer(e),this.fire("layeradd",{layer:e}),this;if(!this._map)return this._needsClustering.push(e),this.fire("layeradd",{layer:e}),this;if(this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom),this.fire("layeradd",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons();var t=e,i=this._zoom;if(e.__parent)for(;t.__parent._zoom>=i;)t=t.__parent;return this._currentShownBounds.contains(t.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,t):this._animationAddLayerNonAnimated(e,t)),this},removeLayer:function(e){return e instanceof L.LayerGroup?this.removeLayers([e]):e.getLatLng?this._map?e.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),this.fire("layerremove",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),e.off(this._childMarkerEventHandlers,this),this._featureGroup.hasLayer(e)&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow()),this):this:(!this._arraySplice(this._needsClustering,e)&&this.hasLayer(e)&&this._needsRemoving.push({layer:e,latlng:e._latlng}),this.fire("layerremove",{layer:e}),this):(this._nonPointGroup.removeLayer(e),this.fire("layerremove",{layer:e}),this)},addLayers:function(e,t){if(!L.Util.isArray(e))return this.addLayer(e);var i,n=this._featureGroup,r=this._nonPointGroup,s=this.options.chunkedLoading,o=this.options.chunkInterval,a=this.options.chunkProgress,h=e.length,l=0,u=!0;if(this._map){var _=(new Date).getTime(),d=L.bind(function(){for(var c=(new Date).getTime();h>l;l++){if(s&&0===l%200){var p=(new Date).getTime()-c;if(p>o)break}if(i=e[l],i instanceof L.LayerGroup)u&&(e=e.slice(),u=!1),this._extractNonGroupLayers(i,e),h=e.length;else if(i.getLatLng){if(!this.hasLayer(i)&&(this._addLayer(i,this._maxZoom),t||this.fire("layeradd",{layer:i}),i.__parent&&2===i.__parent.getChildCount())){var f=i.__parent.getAllChildMarkers(),m=f[0]===i?f[1]:f[0];n.removeLayer(m)}}else r.addLayer(i),t||this.fire("layeradd",{layer:i})}a&&a(l,h,(new Date).getTime()-_),l===h?(this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)):setTimeout(d,this.options.chunkDelay)},this);d()}else for(var c=this._needsClustering;h>l;l++)i=e[l],i instanceof L.LayerGroup?(u&&(e=e.slice(),u=!1),this._extractNonGroupLayers(i,e),h=e.length):i.getLatLng?this.hasLayer(i)||c.push(i):r.addLayer(i);return this},removeLayers:function(e){var t,i,n=e.length,r=this._featureGroup,s=this._nonPointGroup,o=!0;if(!this._map){for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):(this._arraySplice(this._needsClustering,i),s.removeLayer(i),this.hasLayer(i)&&this._needsRemoving.push({layer:i,latlng:i._latlng}),this.fire("layerremove",{layer:i}));return this}if(this._unspiderfy){this._unspiderfy();var a=e.slice(),h=n;for(t=0;h>t;t++)i=a[t],i instanceof L.LayerGroup?(this._extractNonGroupLayers(i,a),h=a.length):this._unspiderfyLayer(i)}for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):i.__parent?(this._removeLayer(i,!0,!0),this.fire("layerremove",{layer:i}),r.hasLayer(i)&&(r.removeLayer(i),i.clusterShow&&i.clusterShow())):(s.removeLayer(i),this.fire("layerremove",{layer:i}));return this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),this},clearLayers:function(){return this._map||(this._needsClustering=[],this._needsRemoving=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(e){e.off(this._childMarkerEventHandlers,this),delete e.__parent},this),this._map&&this._generateInitialClusters(),this},getBounds:function(){var e=new L.LatLngBounds;this._topClusterLevel&&e.extend(this._topClusterLevel._bounds);for(var t=this._needsClustering.length-1;t>=0;t--)e.extend(this._needsClustering[t].getLatLng());return e.extend(this._nonPointGroup.getBounds()),e},eachLayer:function(e,t){var i,n,r,s=this._needsClustering.slice(),o=this._needsRemoving;for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(s),n=s.length-1;n>=0;n--){for(i=!0,r=o.length-1;r>=0;r--)if(o[r].layer===s[n]){i=!1;break}i&&e.call(t,s[n])}this._nonPointGroup.eachLayer(e,t)},getLayers:function(){var e=[];return this.eachLayer(function(t){e.push(t)}),e},getLayer:function(e){var t=null;return e=parseInt(e,10),this.eachLayer(function(i){L.stamp(i)===e&&(t=i)}),t},hasLayer:function(e){if(!e)return!1;var t,i=this._needsClustering;for(t=i.length-1;t>=0;t--)if(i[t]===e)return!0;for(i=this._needsRemoving,t=i.length-1;t>=0;t--)if(i[t].layer===e)return!1;return!(!e.__parent||e.__parent._group!==this)||this._nonPointGroup.hasLayer(e)},zoomToShowLayer:function(e,t){"function"!=typeof t&&(t=function(){});var i=function(){!e._icon&&!e.__parent._icon||this._inZoomAnimation||(this._map.off("moveend",i,this),this.off("animationend",i,this),e._icon?t():e.__parent._icon&&(this.once("spiderfied",t,this),e.__parent.spiderfy()))};e._icon&&this._map.getBounds().contains(e.getLatLng())?t():e.__parent._zoomt;t++)n=this._needsRemoving[t],n.newlatlng=n.layer._latlng,n.layer._latlng=n.latlng;for(t=0,i=this._needsRemoving.length;i>t;t++)n=this._needsRemoving[t],this._removeLayer(n.layer,!0),n.layer._latlng=n.newlatlng;this._needsRemoving=[],this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds(),this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),i=this._needsClustering,this._needsClustering=[],this.addLayers(i,!0)},onRemove:function(e){e.off("zoomend",this._zoomEnd,this),e.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),delete this._maxLat,this._hideCoverage(),this._featureGroup.remove(),this._nonPointGroup.remove(),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(e){for(var t=e;t&&!t._icon;)t=t.__parent;return t||null},_arraySplice:function(e,t){for(var i=e.length-1;i>=0;i--)if(e[i]===t)return e.splice(i,1),!0},_removeFromGridUnclustered:function(e,t){for(var i=this._map,n=this._gridUnclustered,r=Math.floor(this._map.getMinZoom());t>=r&&n[t].removeObject(e,i.project(e.getLatLng(),t));t--);},_childMarkerDragStart:function(e){e.target.__dragStart=e.target._latlng},_childMarkerMoved:function(e){if(!this._ignoreMove&&!e.target.__dragStart){var t=e.target._popup&&e.target._popup.isOpen();this._moveChild(e.target,e.oldLatLng,e.latlng),t&&e.target.openPopup()}},_moveChild:function(e,t,i){e._latlng=t,this.removeLayer(e),e._latlng=i,this.addLayer(e)},_childMarkerDragEnd:function(e){var t=e.target.__dragStart;delete e.target.__dragStart,t&&this._moveChild(e.target,t,e.target._latlng)},_removeLayer:function(e,t,i){var n=this._gridClusters,r=this._gridUnclustered,s=this._featureGroup,o=this._map,a=Math.floor(this._map.getMinZoom());t&&this._removeFromGridUnclustered(e,this._maxZoom);var h,l=e.__parent,u=l._markers;for(this._arraySplice(u,e);l&&(l._childCount--,l._boundsNeedUpdate=!0,!(l._zoomt?"small":100>t?"medium":"large",new L.DivIcon({html:"
"+t+"
",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=this._map,t=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(t||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),e.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(e){for(var t=e.layer,i=t;1===i._childClusters.length;)i=i._childClusters[0];i._zoom===this._maxZoom&&i._childCount===t._childCount&&this.options.spiderfyOnMaxZoom?t.spiderfy():this.options.zoomToBoundsOnClick&&t.zoomToBounds(),e.originalEvent&&13===e.originalEvent.keyCode&&this._map._container.focus()},_showCoverage:function(e){var t=this._map;this._inZoomAnimation||(this._shownPolygon&&t.removeLayer(this._shownPolygon),e.layer.getChildCount()>2&&e.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(e.layer.getConvexHull(),this.options.polygonOptions),t.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var e=this.options.spiderfyOnMaxZoom,t=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(e||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),t&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var e=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,Math.floor(this._map.getMinZoom()),this._zoom,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,Math.round(this._map._zoom),e),this._currentShownBounds=e}},_generateInitialClusters:function(){var e=Math.ceil(this._map.getMaxZoom()),t=Math.floor(this._map.getMinZoom()),i=this.options.maxClusterRadius,n=i;"function"!=typeof i&&(n=function(){return i}),null!==this.options.disableClusteringAtZoom&&(e=this.options.disableClusteringAtZoom-1),this._maxZoom=e,this._gridClusters={},this._gridUnclustered={};for(var r=e;r>=t;r--)this._gridClusters[r]=new L.DistanceGrid(n(r)),this._gridUnclustered[r]=new L.DistanceGrid(n(r));this._topClusterLevel=new this._markerCluster(this,t-1)},_addLayer:function(e,t){var i,n,r=this._gridClusters,s=this._gridUnclustered,o=Math.floor(this._map.getMinZoom());for(this.options.singleMarkerMode&&this._overrideMarkerIcon(e),e.on(this._childMarkerEventHandlers,this);t>=o;t--){i=this._map.project(e.getLatLng(),t);var a=r[t].getNearObject(i);if(a)return a._addChild(e),e.__parent=a,void 0;if(a=s[t].getNearObject(i)){var h=a.__parent;h&&this._removeLayer(a,!1);var l=new this._markerCluster(this,t,a,e);r[t].addObject(l,this._map.project(l._cLatLng,t)),a.__parent=l,e.__parent=l;var u=l;for(n=t-1;n>h._zoom;n--)u=new this._markerCluster(this,n,u),r[n].addObject(u,this._map.project(a.getLatLng(),n));return h._addChild(u),this._removeFromGridUnclustered(a,t),void 0}s[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel},_refreshClustersIcons:function(){this._featureGroup.eachLayer(function(e){e instanceof L.MarkerCluster&&e._iconNeedsUpdate&&e._updateIcon()})},_enqueue:function(e){this._queue.push(e),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var e=0;ee?(this._animationStart(),this._animationZoomOut(this._zoom,e)):this._moveEnd()},_getExpandedVisibleBounds:function(){return this.options.removeOutsideVisibleBounds?L.Browser.mobile?this._checkBoundsMaxLat(this._map.getBounds()):this._checkBoundsMaxLat(this._map.getBounds().pad(1)):this._mapBoundsInfinite},_checkBoundsMaxLat:function(e){var t=this._maxLat;return void 0!==t&&(e.getNorth()>=t&&(e._northEast.lat=1/0),e.getSouth()<=-t&&(e._southWest.lat=-1/0)),e},_animationAddLayerNonAnimated:function(e,t){if(t===e)this._featureGroup.addLayer(e);else if(2===t._childCount){t._addToMap();var i=t.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else t._updateIcon()},_extractNonGroupLayers:function(e,t){var i,n=e.getLayers(),r=0;for(t=t||[];r=0;i--)o=h[i],n.contains(o._latlng)||r.removeLayer(o)}),this._forceLayout(),this._topClusterLevel._recursivelyBecomeVisible(n,t),r.eachLayer(function(e){e instanceof L.MarkerCluster||!e._icon||e.clusterShow()}),this._topClusterLevel._recursively(n,e,t,function(e){e._recursivelyRestoreChildPositions(t)}),this._ignoreMove=!1,this._enqueue(function(){this._topClusterLevel._recursively(n,e,s,function(e){r.removeLayer(e),e.clusterShow()}),this._animationEnd()})},_animationZoomOut:function(e,t){this._animationZoomOutSingle(this._topClusterLevel,e-1,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,Math.floor(this._map.getMinZoom()),e,this._getExpandedVisibleBounds())},_animationAddLayer:function(e,t){var i=this,n=this._featureGroup;n.addLayer(e),t!==e&&(t._childCount>2?(t._updateIcon(),this._forceLayout(),this._animationStart(),e._setPos(this._map.latLngToLayerPoint(t.getLatLng())),e.clusterHide(),this._enqueue(function(){n.removeLayer(e),e.clusterShow(),i._animationEnd()})):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(t,this._map.getMaxZoom(),this._zoom)))}},_animationZoomOutSingle:function(e,t,i){var n=this._getExpandedVisibleBounds(),r=Math.floor(this._map.getMinZoom());e._recursivelyAnimateChildrenInAndAddSelfToMap(n,r,t+1,i);var s=this;this._forceLayout(),e._recursivelyBecomeVisible(n,i),this._enqueue(function(){if(1===e._childCount){var o=e._markers[0];this._ignoreMove=!0,o.setLatLng(o.getLatLng()),this._ignoreMove=!1,o.clusterShow&&o.clusterShow()}else e._recursively(n,i,r,function(e){e._recursivelyRemoveChildrenFromMap(n,r,t+1)});s._animationEnd()})},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_forceLayout:function(){L.Util.falseFn(document.body.offsetWidth)}}),L.markerClusterGroup=function(e){return new L.MarkerClusterGroup(e)};var i=L.MarkerCluster=L.Marker.extend({options:L.Icon.prototype.options,initialize:function(e,t,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this,pane:e.options.clusterPane}),this._group=e,this._zoom=t,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(e,t){e=e||[];for(var i=this._childClusters.length-1;i>=0;i--)this._childClusters[i].getAllChildMarkers(e);for(var n=this._markers.length-1;n>=0;n--)t&&this._markers[n].__dragStart||e.push(this._markers[n]);return e},getChildCount:function(){return this._childCount},zoomToBounds:function(e){for(var t,i=this._childClusters.slice(),n=this._group._map,r=n.getBoundsZoom(this._bounds),s=this._zoom+1,o=n.getZoom();i.length>0&&r>s;){s++;var a=[];for(t=0;ts?this._group._map.setView(this._latlng,s):o>=r?this._group._map.setView(this._latlng,o+1):this._group._map.fitBounds(this._bounds,e)},getBounds:function(){var e=new L.LatLngBounds;return e.extend(this._bounds),e},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(e,t){this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._setClusterCenter(e),e instanceof L.MarkerCluster?(t||(this._childClusters.push(e),e.__parent=this),this._childCount+=e._childCount):(t||this._markers.push(e),this._childCount++),this.__parent&&this.__parent._addChild(e,!0)},_setClusterCenter:function(e){this._cLatLng||(this._cLatLng=e._cLatLng||e._latlng)},_resetBounds:function(){var e=this._bounds;e._southWest&&(e._southWest.lat=1/0,e._southWest.lng=1/0),e._northEast&&(e._northEast.lat=-1/0,e._northEast.lng=-1/0)},_recalculateBounds:function(){var e,t,i,n,r=this._markers,s=this._childClusters,o=0,a=0,h=this._childCount;if(0!==h){for(this._resetBounds(),e=0;e=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())},function(e){var i,n,r=e._childClusters;for(i=r.length-1;i>=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(e,t,i,n){this._recursively(e,n,t,function(r){r._recursivelyAnimateChildrenIn(e,r._group._map.latLngToLayerPoint(r.getLatLng()).round(),i),r._isSingleParent()&&i-1===n?(r.clusterShow(),r._recursivelyRemoveChildrenFromMap(e,t,i)):r.clusterHide(),r._addToMap()})},_recursivelyBecomeVisible:function(e,t){this._recursively(e,this._group._map.getMinZoom(),t,null,function(e){e.clusterShow()})},_recursivelyAddChildrenToMap:function(e,t,i){this._recursively(i,this._group._map.getMinZoom()-1,t,function(n){if(t!==n._zoom)for(var r=n._markers.length-1;r>=0;r--){var s=n._markers[r];i.contains(s._latlng)&&(e&&(s._backupLatlng=s.getLatLng(),s.setLatLng(e),s.clusterHide&&s.clusterHide()),n._group._featureGroup.addLayer(s))}},function(t){t._addToMap(e)})},_recursivelyRestoreChildPositions:function(e){for(var t=this._markers.length-1;t>=0;t--){var i=this._markers[t];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(e-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var r=this._childClusters.length-1;r>=0;r--)this._childClusters[r]._recursivelyRestoreChildPositions(e)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(e,t,i,n){var r,s;this._recursively(e,t-1,i-1,function(e){for(s=e._markers.length-1;s>=0;s--)r=e._markers[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())},function(e){for(s=e._childClusters.length-1;s>=0;s--)r=e._childClusters[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())})},_recursively:function(e,t,i,n,r){var s,o,a=this._childClusters,h=this._zoom;if(h>=t&&(n&&n(this),r&&h===i&&r(this)),t>h||i>h)for(s=a.length-1;s>=0;s--)o=a[s],o._boundsNeedUpdate&&o._recalculateBounds(),e.intersects(o._bounds)&&o._recursively(e,t,i,n,r)},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}});L.Marker.include({clusterHide:function(){var e=this.options.opacity;return this.setOpacity(0),this.options.opacity=e,this},clusterShow:function(){return this.setOpacity(this.options.opacity)}}),L.DistanceGrid=function(e){this._cellSize=e,this._sqCellSize=e*e,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(e,t){var i=this._getCoord(t.x),n=this._getCoord(t.y),r=this._grid,s=r[n]=r[n]||{},o=s[i]=s[i]||[],a=L.Util.stamp(e);this._objectPoint[a]=t,o.push(e)},updateObject:function(e,t){this.removeObject(e),this.addObject(e,t)},removeObject:function(e,t){var i,n,r=this._getCoord(t.x),s=this._getCoord(t.y),o=this._grid,a=o[s]=o[s]||{},h=a[r]=a[r]||[];for(delete this._objectPoint[L.Util.stamp(e)],i=0,n=h.length;n>i;i++)if(h[i]===e)return h.splice(i,1),1===n&&delete a[r],!0},eachObject:function(e,t){var i,n,r,s,o,a,h,l=this._grid;for(i in l){o=l[i];for(n in o)for(a=o[n],r=0,s=a.length;s>r;r++)h=e.call(t,a[r]),h&&(r--,s--)}},getNearObject:function(e){var t,i,n,r,s,o,a,h,l=this._getCoord(e.x),u=this._getCoord(e.y),_=this._objectPoint,d=this._sqCellSize,c=null;for(t=u-1;u+1>=t;t++)if(r=this._grid[t])for(i=l-1;l+1>=i;i++)if(s=r[i])for(n=0,o=s.length;o>n;n++)a=s[n],h=this._sqDist(_[L.Util.stamp(a)],e),(d>h||d>=h&&null===c)&&(d=h,c=a);return c},_getCoord:function(e){var t=Math.floor(e/this._cellSize);return isFinite(t)?t:e},_sqDist:function(e,t){var i=t.x-e.x,n=t.y-e.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(e,t){var i=t[1].lat-t[0].lat,n=t[0].lng-t[1].lng;return n*(e.lat-t[0].lat)+i*(e.lng-t[0].lng)},findMostDistantPointFromBaseLine:function(e,t){var i,n,r,s=0,o=null,a=[];for(i=t.length-1;i>=0;i--)n=t[i],r=this.getDistant(n,e),r>0&&(a.push(n),r>s&&(s=r,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(e,t){var i=[],n=this.findMostDistantPointFromBaseLine(e,t);return n.maxPoint?(i=i.concat(this.buildConvexHull([e[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,e[1]],n.newPoints))):[e[0]]},getConvexHull:function(e){var t,i=!1,n=!1,r=!1,s=!1,o=null,a=null,h=null,l=null,u=null,_=null;for(t=e.length-1;t>=0;t--){var d=e[t];(i===!1||d.lat>i)&&(o=d,i=d.lat),(n===!1||d.latr)&&(h=d,r=d.lng),(s===!1||d.lng=0;t--)e=i[t].getLatLng(),n.push(e);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:0,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var e,t=this.getAllChildMarkers(null,!0),i=this._group,n=i._map,r=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,t.length>=this._circleSpiralSwitchover?e=this._generatePointsSpiral(t.length,r):(r.y+=10,e=this._generatePointsCircle(t.length,r)),this._animationSpiderfy(t,e)}},unspiderfy:function(e){this._group._inZoomAnimation||(this._animationUnspiderfy(e),this._group._spiderfied=null)},_generatePointsCircle:function(e,t){var i,n,r=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e),s=r/this._2PI,o=this._2PI/e,a=[];for(s=Math.max(s,35),a.length=e,i=0;e>i;i++)n=this._circleStartAngle+i*o,a[i]=new L.Point(t.x+s*Math.cos(n),t.y+s*Math.sin(n))._round();return a},_generatePointsSpiral:function(e,t){var i,n=this._group.options.spiderfyDistanceMultiplier,r=n*this._spiralLengthStart,s=n*this._spiralFootSeparation,o=n*this._spiralLengthFactor*this._2PI,a=0,h=[];for(h.length=e,i=e;i>=0;i--)e>i&&(h[i]=new L.Point(t.x+r*Math.cos(a),t.y+r*Math.sin(a))._round()),a+=s/r+5e-4*i,r+=o/a;return h},_noanimationUnspiderfy:function(){var e,t,i=this._group,n=i._map,r=i._featureGroup,s=this.getAllChildMarkers(null,!0);for(i._ignoreMove=!0,this.setOpacity(1),t=s.length-1;t>=0;t--)e=s[t],r.removeLayer(e),e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng),e.setZIndexOffset&&e.setZIndexOffset(0),e._spiderLeg&&(n.removeLayer(e._spiderLeg),delete e._spiderLeg);i.fire("unspiderfied",{cluster:this,markers:s}),i._ignoreMove=!1,i._spiderfied=null}}),L.MarkerClusterNonAnimated=L.MarkerCluster.extend({_animationSpiderfy:function(e,t){var i,n,r,s,o=this._group,a=o._map,h=o._featureGroup,l=this._group.options.spiderLegPolylineOptions;for(o._ignoreMove=!0,i=0;i=0;i--)a=u.layerPointToLatLng(t[i]),n=e[i],n._preSpiderfyLatlng=n._latlng,n.setLatLng(a),n.clusterShow&&n.clusterShow(),p&&(r=n._spiderLeg,s=r._path,s.style.strokeDashoffset=0,r.setStyle({opacity:m}));this.setOpacity(.3),l._ignoreMove=!1,setTimeout(function(){l._animationEnd(),l.fire("spiderfied",{cluster:h,markers:e})},200)},_animationUnspiderfy:function(e){var t,i,n,r,s,o,a=this,h=this._group,l=h._map,u=h._featureGroup,_=e?l._latLngToNewLayerPoint(this._latlng,e.zoom,e.center):l.latLngToLayerPoint(this._latlng),d=this.getAllChildMarkers(null,!0),c=L.Path.SVG;for(h._ignoreMove=!0,h._animationStart(),this.setOpacity(1),i=d.length-1;i>=0;i--)t=d[i],t._preSpiderfyLatlng&&(t.closePopup(),t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng,o=!0,t._setPos&&(t._setPos(_),o=!1),t.clusterHide&&(t.clusterHide(),o=!1),o&&u.removeLayer(t),c&&(n=t._spiderLeg,r=n._path,s=r.getTotalLength()+.1,r.style.strokeDashoffset=s,n.setStyle({opacity:0})));h._ignoreMove=!1,setTimeout(function(){var e=0;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&e++;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&(t.clusterShow&&t.clusterShow(),t.setZIndexOffset&&t.setZIndexOffset(0),e>1&&u.removeLayer(t),l.removeLayer(t._spiderLeg),delete t._spiderLeg);h._animationEnd(),h.fire("unspiderfied",{cluster:a,markers:d})},200)}}),L.MarkerClusterGroup.include({_spiderfied:null,unspiderfy:function(){this._unspiderfy.apply(this,arguments)},_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Browser.touch||this._map.getRenderer(this)},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._map.off("zoomend",this._noanimationUnspiderfy,this),this._noanimationUnspiderfy() 2 | },_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(e){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(e))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(e){this._spiderfied&&this._spiderfied.unspiderfy(e)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(e){e._spiderLeg&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow(),e.setZIndexOffset&&e.setZIndexOffset(0),this._map.removeLayer(e._spiderLeg),delete e._spiderLeg)}}),L.MarkerClusterGroup.include({refreshClusters:function(e){return e?e instanceof L.MarkerClusterGroup?e=e._topClusterLevel.getAllChildMarkers():e instanceof L.LayerGroup?e=e._layers:e instanceof L.MarkerCluster?e=e.getAllChildMarkers():e instanceof L.Marker&&(e=[e]):e=this._topClusterLevel.getAllChildMarkers(),this._flagParentsIconsNeedUpdate(e),this._refreshClustersIcons(),this.options.singleMarkerMode&&this._refreshSingleMarkerModeMarkers(e),this},_flagParentsIconsNeedUpdate:function(e){var t,i;for(t in e)for(i=e[t].__parent;i;)i._iconNeedsUpdate=!0,i=i.__parent},_refreshSingleMarkerModeMarkers:function(e){var t,i;for(t in e)i=e[t],this.hasLayer(i)&&i.setIcon(this._overrideMarkerIcon(i))}}),L.Marker.include({refreshIconOptions:function(e,t){var i=this.options.icon;return L.setOptions(i,e),this.setIcon(i),t&&this.__parent&&this.__parent._group.refreshClusters(this),this}}),e.MarkerClusterGroup=t,e.MarkerCluster=i}); 3 | //# sourceMappingURL=leaflet.markercluster.js.map -------------------------------------------------------------------------------- /vendor/assets/stylesheets/MarkerCluster.Default.css: -------------------------------------------------------------------------------- 1 | .marker-cluster-small { 2 | background-color: rgba(181, 226, 140, 0.6); 3 | } 4 | .marker-cluster-small div { 5 | background-color: rgba(110, 204, 57, 0.6); 6 | } 7 | 8 | .marker-cluster-medium { 9 | background-color: rgba(241, 211, 87, 0.6); 10 | } 11 | .marker-cluster-medium div { 12 | background-color: rgba(240, 194, 12, 0.6); 13 | } 14 | 15 | .marker-cluster-large { 16 | background-color: rgba(253, 156, 115, 0.6); 17 | } 18 | .marker-cluster-large div { 19 | background-color: rgba(241, 128, 23, 0.6); 20 | } 21 | 22 | /* IE 6-8 fallback colors */ 23 | .leaflet-oldie .marker-cluster-small { 24 | background-color: rgb(181, 226, 140); 25 | } 26 | .leaflet-oldie .marker-cluster-small div { 27 | background-color: rgb(110, 204, 57); 28 | } 29 | 30 | .leaflet-oldie .marker-cluster-medium { 31 | background-color: rgb(241, 211, 87); 32 | } 33 | .leaflet-oldie .marker-cluster-medium div { 34 | background-color: rgb(240, 194, 12); 35 | } 36 | 37 | .leaflet-oldie .marker-cluster-large { 38 | background-color: rgb(253, 156, 115); 39 | } 40 | .leaflet-oldie .marker-cluster-large div { 41 | background-color: rgb(241, 128, 23); 42 | } 43 | 44 | .marker-cluster { 45 | background-clip: padding-box; 46 | border-radius: 20px; 47 | } 48 | .marker-cluster div { 49 | width: 30px; 50 | height: 30px; 51 | margin-left: 5px; 52 | margin-top: 5px; 53 | 54 | text-align: center; 55 | border-radius: 15px; 56 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; 57 | } 58 | .marker-cluster span { 59 | line-height: 30px; 60 | } -------------------------------------------------------------------------------- /vendor/assets/stylesheets/MarkerCluster.css: -------------------------------------------------------------------------------- 1 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { 2 | -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; 3 | -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; 4 | -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; 5 | transition: transform 0.3s ease-out, opacity 0.3s ease-in; 6 | } 7 | 8 | .leaflet-cluster-spider-leg { 9 | /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */ 10 | -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in; 11 | -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in; 12 | -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in; 13 | transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in; 14 | } 15 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/leaflet.css: -------------------------------------------------------------------------------- 1 | /* required styles */ 2 | 3 | .leaflet-pane, 4 | .leaflet-tile, 5 | .leaflet-marker-icon, 6 | .leaflet-marker-shadow, 7 | .leaflet-tile-container, 8 | .leaflet-pane > svg, 9 | .leaflet-pane > canvas, 10 | .leaflet-zoom-box, 11 | .leaflet-image-layer, 12 | .leaflet-layer { 13 | position: absolute; 14 | left: 0; 15 | top: 0; 16 | } 17 | .leaflet-container { 18 | overflow: hidden; 19 | } 20 | .leaflet-tile, 21 | .leaflet-marker-icon, 22 | .leaflet-marker-shadow { 23 | -webkit-user-select: none; 24 | -moz-user-select: none; 25 | user-select: none; 26 | -webkit-user-drag: none; 27 | } 28 | /* Safari renders non-retina tile on retina better with this, but Chrome is worse */ 29 | .leaflet-safari .leaflet-tile { 30 | image-rendering: -webkit-optimize-contrast; 31 | } 32 | /* hack that prevents hw layers "stretching" when loading new tiles */ 33 | .leaflet-safari .leaflet-tile-container { 34 | width: 1600px; 35 | height: 1600px; 36 | -webkit-transform-origin: 0 0; 37 | } 38 | .leaflet-marker-icon, 39 | .leaflet-marker-shadow { 40 | display: block; 41 | } 42 | /* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ 43 | /* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ 44 | .leaflet-container .leaflet-overlay-pane svg, 45 | .leaflet-container .leaflet-marker-pane img, 46 | .leaflet-container .leaflet-shadow-pane img, 47 | .leaflet-container .leaflet-tile-pane img, 48 | .leaflet-container img.leaflet-image-layer, 49 | .leaflet-container .leaflet-tile { 50 | max-width: none !important; 51 | max-height: none !important; 52 | } 53 | 54 | .leaflet-container.leaflet-touch-zoom { 55 | -ms-touch-action: pan-x pan-y; 56 | touch-action: pan-x pan-y; 57 | } 58 | .leaflet-container.leaflet-touch-drag { 59 | -ms-touch-action: pinch-zoom; 60 | /* Fallback for FF which doesn't support pinch-zoom */ 61 | touch-action: none; 62 | touch-action: pinch-zoom; 63 | } 64 | .leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { 65 | -ms-touch-action: none; 66 | touch-action: none; 67 | } 68 | .leaflet-container { 69 | -webkit-tap-highlight-color: transparent; 70 | } 71 | .leaflet-container a { 72 | -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); 73 | } 74 | .leaflet-tile { 75 | filter: inherit; 76 | visibility: hidden; 77 | } 78 | .leaflet-tile-loaded { 79 | visibility: inherit; 80 | } 81 | .leaflet-zoom-box { 82 | width: 0; 83 | height: 0; 84 | -moz-box-sizing: border-box; 85 | box-sizing: border-box; 86 | z-index: 800; 87 | } 88 | /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ 89 | .leaflet-overlay-pane svg { 90 | -moz-user-select: none; 91 | } 92 | 93 | .leaflet-pane { z-index: 400; } 94 | 95 | .leaflet-tile-pane { z-index: 200; } 96 | .leaflet-overlay-pane { z-index: 400; } 97 | .leaflet-shadow-pane { z-index: 500; } 98 | .leaflet-marker-pane { z-index: 600; } 99 | .leaflet-tooltip-pane { z-index: 650; } 100 | .leaflet-popup-pane { z-index: 700; } 101 | 102 | .leaflet-map-pane canvas { z-index: 100; } 103 | .leaflet-map-pane svg { z-index: 200; } 104 | 105 | .leaflet-vml-shape { 106 | width: 1px; 107 | height: 1px; 108 | } 109 | .lvml { 110 | behavior: url(#default#VML); 111 | display: inline-block; 112 | position: absolute; 113 | } 114 | 115 | 116 | /* control positioning */ 117 | 118 | .leaflet-control { 119 | position: relative; 120 | z-index: 800; 121 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ 122 | pointer-events: auto; 123 | } 124 | .leaflet-top, 125 | .leaflet-bottom { 126 | position: absolute; 127 | z-index: 1000; 128 | pointer-events: none; 129 | } 130 | .leaflet-top { 131 | top: 0; 132 | } 133 | .leaflet-right { 134 | right: 0; 135 | } 136 | .leaflet-bottom { 137 | bottom: 0; 138 | } 139 | .leaflet-left { 140 | left: 0; 141 | } 142 | .leaflet-control { 143 | float: left; 144 | clear: both; 145 | } 146 | .leaflet-right .leaflet-control { 147 | float: right; 148 | } 149 | .leaflet-top .leaflet-control { 150 | margin-top: 10px; 151 | } 152 | .leaflet-bottom .leaflet-control { 153 | margin-bottom: 10px; 154 | } 155 | .leaflet-left .leaflet-control { 156 | margin-left: 10px; 157 | } 158 | .leaflet-right .leaflet-control { 159 | margin-right: 10px; 160 | } 161 | 162 | 163 | /* zoom and fade animations */ 164 | 165 | .leaflet-fade-anim .leaflet-tile { 166 | will-change: opacity; 167 | } 168 | .leaflet-fade-anim .leaflet-popup { 169 | opacity: 0; 170 | -webkit-transition: opacity 0.2s linear; 171 | -moz-transition: opacity 0.2s linear; 172 | transition: opacity 0.2s linear; 173 | } 174 | .leaflet-fade-anim .leaflet-map-pane .leaflet-popup { 175 | opacity: 1; 176 | } 177 | .leaflet-zoom-animated { 178 | -webkit-transform-origin: 0 0; 179 | -ms-transform-origin: 0 0; 180 | transform-origin: 0 0; 181 | } 182 | .leaflet-zoom-anim .leaflet-zoom-animated { 183 | will-change: transform; 184 | } 185 | .leaflet-zoom-anim .leaflet-zoom-animated { 186 | -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); 187 | -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); 188 | transition: transform 0.25s cubic-bezier(0,0,0.25,1); 189 | } 190 | .leaflet-zoom-anim .leaflet-tile, 191 | .leaflet-pan-anim .leaflet-tile { 192 | -webkit-transition: none; 193 | -moz-transition: none; 194 | transition: none; 195 | } 196 | 197 | .leaflet-zoom-anim .leaflet-zoom-hide { 198 | visibility: hidden; 199 | } 200 | 201 | 202 | /* cursors */ 203 | 204 | .leaflet-interactive { 205 | cursor: pointer; 206 | } 207 | .leaflet-grab { 208 | cursor: -webkit-grab; 209 | cursor: -moz-grab; 210 | cursor: grab; 211 | } 212 | .leaflet-crosshair, 213 | .leaflet-crosshair .leaflet-interactive { 214 | cursor: crosshair; 215 | } 216 | .leaflet-popup-pane, 217 | .leaflet-control { 218 | cursor: auto; 219 | } 220 | .leaflet-dragging .leaflet-grab, 221 | .leaflet-dragging .leaflet-grab .leaflet-interactive, 222 | .leaflet-dragging .leaflet-marker-draggable { 223 | cursor: move; 224 | cursor: -webkit-grabbing; 225 | cursor: -moz-grabbing; 226 | cursor: grabbing; 227 | } 228 | 229 | /* marker & overlays interactivity */ 230 | .leaflet-marker-icon, 231 | .leaflet-marker-shadow, 232 | .leaflet-image-layer, 233 | .leaflet-pane > svg path, 234 | .leaflet-tile-container { 235 | pointer-events: none; 236 | } 237 | 238 | .leaflet-marker-icon.leaflet-interactive, 239 | .leaflet-image-layer.leaflet-interactive, 240 | .leaflet-pane > svg path.leaflet-interactive { 241 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ 242 | pointer-events: auto; 243 | } 244 | 245 | /* visual tweaks */ 246 | 247 | .leaflet-container { 248 | background: #ddd; 249 | outline: 0; 250 | } 251 | .leaflet-container a { 252 | color: #0078A8; 253 | } 254 | .leaflet-container a.leaflet-active { 255 | outline: 2px solid orange; 256 | } 257 | .leaflet-zoom-box { 258 | border: 2px dotted #38f; 259 | background: rgba(255,255,255,0.5); 260 | } 261 | 262 | 263 | /* general typography */ 264 | .leaflet-container { 265 | font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; 266 | } 267 | 268 | 269 | /* general toolbar styles */ 270 | 271 | .leaflet-bar { 272 | box-shadow: 0 1px 5px rgba(0,0,0,0.65); 273 | border-radius: 4px; 274 | } 275 | .leaflet-bar a, 276 | .leaflet-bar a:hover { 277 | background-color: #fff; 278 | border-bottom: 1px solid #ccc; 279 | width: 26px; 280 | height: 26px; 281 | line-height: 26px; 282 | display: block; 283 | text-align: center; 284 | text-decoration: none; 285 | color: black; 286 | } 287 | .leaflet-bar a, 288 | .leaflet-control-layers-toggle { 289 | background-position: 50% 50%; 290 | background-repeat: no-repeat; 291 | display: block; 292 | } 293 | .leaflet-bar a:hover { 294 | background-color: #f4f4f4; 295 | } 296 | .leaflet-bar a:first-child { 297 | border-top-left-radius: 4px; 298 | border-top-right-radius: 4px; 299 | } 300 | .leaflet-bar a:last-child { 301 | border-bottom-left-radius: 4px; 302 | border-bottom-right-radius: 4px; 303 | border-bottom: none; 304 | } 305 | .leaflet-bar a.leaflet-disabled { 306 | cursor: default; 307 | background-color: #f4f4f4; 308 | color: #bbb; 309 | } 310 | 311 | .leaflet-touch .leaflet-bar a { 312 | width: 30px; 313 | height: 30px; 314 | line-height: 30px; 315 | } 316 | .leaflet-touch .leaflet-bar a:first-child { 317 | border-top-left-radius: 2px; 318 | border-top-right-radius: 2px; 319 | } 320 | .leaflet-touch .leaflet-bar a:last-child { 321 | border-bottom-left-radius: 2px; 322 | border-bottom-right-radius: 2px; 323 | } 324 | 325 | /* zoom control */ 326 | 327 | .leaflet-control-zoom-in, 328 | .leaflet-control-zoom-out { 329 | font: bold 18px 'Lucida Console', Monaco, monospace; 330 | text-indent: 1px; 331 | } 332 | 333 | .leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { 334 | font-size: 22px; 335 | } 336 | 337 | 338 | /* layers control */ 339 | 340 | .leaflet-control-layers { 341 | box-shadow: 0 1px 5px rgba(0,0,0,0.4); 342 | background: #fff; 343 | border-radius: 5px; 344 | } 345 | .leaflet-control-layers-toggle { 346 | background-image: url(layers.png); 347 | width: 36px; 348 | height: 36px; 349 | } 350 | .leaflet-retina .leaflet-control-layers-toggle { 351 | background-image: url(layers-2x.png); 352 | background-size: 26px 26px; 353 | } 354 | .leaflet-touch .leaflet-control-layers-toggle { 355 | width: 44px; 356 | height: 44px; 357 | } 358 | .leaflet-control-layers .leaflet-control-layers-list, 359 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle { 360 | display: none; 361 | } 362 | .leaflet-control-layers-expanded .leaflet-control-layers-list { 363 | display: block; 364 | position: relative; 365 | } 366 | .leaflet-control-layers-expanded { 367 | padding: 6px 10px 6px 6px; 368 | color: #333; 369 | background: #fff; 370 | } 371 | .leaflet-control-layers-scrollbar { 372 | overflow-y: scroll; 373 | overflow-x: hidden; 374 | padding-right: 5px; 375 | } 376 | .leaflet-control-layers-selector { 377 | margin-top: 2px; 378 | position: relative; 379 | top: 1px; 380 | } 381 | .leaflet-control-layers label { 382 | display: block; 383 | } 384 | .leaflet-control-layers-separator { 385 | height: 0; 386 | border-top: 1px solid #ddd; 387 | margin: 5px -10px 5px -6px; 388 | } 389 | 390 | /* Default icon URLs */ 391 | .leaflet-default-icon-path { 392 | background-image: url(marker-icon.png); 393 | } 394 | 395 | 396 | /* attribution and scale controls */ 397 | 398 | .leaflet-container .leaflet-control-attribution { 399 | background: #fff; 400 | background: rgba(255, 255, 255, 0.7); 401 | margin: 0; 402 | } 403 | .leaflet-control-attribution, 404 | .leaflet-control-scale-line { 405 | padding: 0 5px; 406 | color: #333; 407 | } 408 | .leaflet-control-attribution a { 409 | text-decoration: none; 410 | } 411 | .leaflet-control-attribution a:hover { 412 | text-decoration: underline; 413 | } 414 | .leaflet-container .leaflet-control-attribution, 415 | .leaflet-container .leaflet-control-scale { 416 | font-size: 11px; 417 | } 418 | .leaflet-left .leaflet-control-scale { 419 | margin-left: 5px; 420 | } 421 | .leaflet-bottom .leaflet-control-scale { 422 | margin-bottom: 5px; 423 | } 424 | .leaflet-control-scale-line { 425 | border: 2px solid #777; 426 | border-top: none; 427 | line-height: 1.1; 428 | padding: 2px 5px 1px; 429 | font-size: 11px; 430 | white-space: nowrap; 431 | overflow: hidden; 432 | -moz-box-sizing: border-box; 433 | box-sizing: border-box; 434 | 435 | background: #fff; 436 | background: rgba(255, 255, 255, 0.5); 437 | } 438 | .leaflet-control-scale-line:not(:first-child) { 439 | border-top: 2px solid #777; 440 | border-bottom: none; 441 | margin-top: -2px; 442 | } 443 | .leaflet-control-scale-line:not(:first-child):not(:last-child) { 444 | border-bottom: 2px solid #777; 445 | } 446 | 447 | .leaflet-touch .leaflet-control-attribution, 448 | .leaflet-touch .leaflet-control-layers, 449 | .leaflet-touch .leaflet-bar { 450 | box-shadow: none; 451 | } 452 | .leaflet-touch .leaflet-control-layers, 453 | .leaflet-touch .leaflet-bar { 454 | border: 2px solid rgba(0,0,0,0.2); 455 | background-clip: padding-box; 456 | } 457 | 458 | 459 | /* popup */ 460 | 461 | .leaflet-popup { 462 | position: absolute; 463 | text-align: center; 464 | margin-bottom: 20px; 465 | } 466 | .leaflet-popup-content-wrapper { 467 | padding: 1px; 468 | text-align: left; 469 | border-radius: 12px; 470 | } 471 | .leaflet-popup-content { 472 | margin: 13px 19px; 473 | line-height: 1.4; 474 | } 475 | .leaflet-popup-content p { 476 | margin: 18px 0; 477 | } 478 | .leaflet-popup-tip-container { 479 | width: 40px; 480 | height: 20px; 481 | position: absolute; 482 | left: 50%; 483 | margin-left: -20px; 484 | overflow: hidden; 485 | pointer-events: none; 486 | } 487 | .leaflet-popup-tip { 488 | width: 17px; 489 | height: 17px; 490 | padding: 1px; 491 | 492 | margin: -10px auto 0; 493 | 494 | -webkit-transform: rotate(45deg); 495 | -moz-transform: rotate(45deg); 496 | -ms-transform: rotate(45deg); 497 | transform: rotate(45deg); 498 | } 499 | .leaflet-popup-content-wrapper, 500 | .leaflet-popup-tip { 501 | background: white; 502 | color: #333; 503 | box-shadow: 0 3px 14px rgba(0,0,0,0.4); 504 | } 505 | .leaflet-container a.leaflet-popup-close-button { 506 | position: absolute; 507 | top: 0; 508 | right: 0; 509 | padding: 4px 4px 0 0; 510 | border: none; 511 | text-align: center; 512 | width: 18px; 513 | height: 14px; 514 | font: 16px/14px Tahoma, Verdana, sans-serif; 515 | color: #c3c3c3; 516 | text-decoration: none; 517 | font-weight: bold; 518 | background: transparent; 519 | } 520 | .leaflet-container a.leaflet-popup-close-button:hover { 521 | color: #999; 522 | } 523 | .leaflet-popup-scrolled { 524 | overflow: auto; 525 | border-bottom: 1px solid #ddd; 526 | border-top: 1px solid #ddd; 527 | } 528 | 529 | .leaflet-oldie .leaflet-popup-content-wrapper { 530 | zoom: 1; 531 | } 532 | .leaflet-oldie .leaflet-popup-tip { 533 | width: 24px; 534 | margin: 0 auto; 535 | 536 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; 537 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); 538 | } 539 | .leaflet-oldie .leaflet-popup-tip-container { 540 | margin-top: -1px; 541 | } 542 | 543 | .leaflet-oldie .leaflet-control-zoom, 544 | .leaflet-oldie .leaflet-control-layers, 545 | .leaflet-oldie .leaflet-popup-content-wrapper, 546 | .leaflet-oldie .leaflet-popup-tip { 547 | border: 1px solid #999; 548 | } 549 | 550 | 551 | /* div icon */ 552 | 553 | .leaflet-div-icon { 554 | background: #fff; 555 | border: 1px solid #666; 556 | } 557 | 558 | 559 | /* Tooltip */ 560 | /* Base styles for the element that has a tooltip */ 561 | .leaflet-tooltip { 562 | position: absolute; 563 | padding: 6px; 564 | background-color: #fff; 565 | border: 1px solid #fff; 566 | border-radius: 3px; 567 | color: #222; 568 | white-space: nowrap; 569 | -webkit-user-select: none; 570 | -moz-user-select: none; 571 | -ms-user-select: none; 572 | user-select: none; 573 | pointer-events: none; 574 | box-shadow: 0 1px 3px rgba(0,0,0,0.4); 575 | } 576 | .leaflet-tooltip.leaflet-clickable { 577 | cursor: pointer; 578 | pointer-events: auto; 579 | } 580 | .leaflet-tooltip-top:before, 581 | .leaflet-tooltip-bottom:before, 582 | .leaflet-tooltip-left:before, 583 | .leaflet-tooltip-right:before { 584 | position: absolute; 585 | pointer-events: none; 586 | border: 6px solid transparent; 587 | background: transparent; 588 | content: ""; 589 | } 590 | 591 | /* Directions */ 592 | 593 | .leaflet-tooltip-bottom { 594 | margin-top: 6px; 595 | } 596 | .leaflet-tooltip-top { 597 | margin-top: -6px; 598 | } 599 | .leaflet-tooltip-bottom:before, 600 | .leaflet-tooltip-top:before { 601 | left: 50%; 602 | margin-left: -6px; 603 | } 604 | .leaflet-tooltip-top:before { 605 | bottom: 0; 606 | margin-bottom: -12px; 607 | border-top-color: #fff; 608 | } 609 | .leaflet-tooltip-bottom:before { 610 | top: 0; 611 | margin-top: -12px; 612 | margin-left: -6px; 613 | border-bottom-color: #fff; 614 | } 615 | .leaflet-tooltip-left { 616 | margin-left: -6px; 617 | } 618 | .leaflet-tooltip-right { 619 | margin-left: 6px; 620 | } 621 | .leaflet-tooltip-left:before, 622 | .leaflet-tooltip-right:before { 623 | top: 50%; 624 | margin-top: -6px; 625 | } 626 | .leaflet-tooltip-left:before { 627 | right: 0; 628 | margin-right: -12px; 629 | border-left-color: #fff; 630 | } 631 | .leaflet-tooltip-right:before { 632 | left: 0; 633 | margin-left: -12px; 634 | border-right-color: #fff; 635 | } 636 | --------------------------------------------------------------------------------