diff --git a/GeolocationPlugin.php b/GeolocationPlugin.php
index 649440f..bf15bec 100644
--- a/GeolocationPlugin.php
+++ b/GeolocationPlugin.php
@@ -58,6 +58,7 @@ public function hookInstall()
`zoom_level` INT NOT NULL ,
`address` TEXT NOT NULL ,
`label` VARCHAR( 255 ) NOT NULL DEFAULT '' ,
+ `geometry_json` TEXT NOT NULL ,
INDEX (`item_id`)) ENGINE = InnoDB";
$db->query($sql);
@@ -71,6 +72,7 @@ public function hookInstall()
set_option('geolocation_basemap', self::DEFAULT_BASEMAP);
set_option('geolocation_geocoder', self::DEFAULT_GEOCODER);
set_option('geolocation_item_map_enable', '1');
+ set_option('geolocation_auto_fit_browse', '1');
}
public function hookUninstall()
@@ -163,7 +165,13 @@ public function hookUpgrade($args)
}
if (version_compare($args['old_version'], '4.0', '<')) {
$db = get_db();
- $db->query("ALTER TABLE `$db->Location` ADD COLUMN `label` VARCHAR(255) NOT NULL DEFAULT '' AFTER `address`, DROP COLUMN `map_type`");
+ // Three steps: add nullable, back-fill from existing lat/lng,
+ // then tighten to NOT NULL. MySQL rejects adding a NOT NULL
+ // column to a non-empty table without a default, and a
+ // placeholder default would corrupt the existing coordinate data.
+ $db->query("ALTER TABLE `$db->Location` ADD COLUMN `label` VARCHAR(255) NOT NULL DEFAULT '' AFTER `address`, DROP COLUMN `map_type`, ADD COLUMN `geometry_json` TEXT NULL");
+ $db->query("UPDATE `$db->Location` SET `geometry_json` = CONCAT('{\"type\":\"Point\",\"coordinates\":[', `longitude`, ',', `latitude`, ']}')");
+ $db->query("ALTER TABLE `$db->Location` MODIFY COLUMN `geometry_json` TEXT NOT NULL");
}
}
@@ -263,13 +271,9 @@ private function _head()
$version = Zend_Registry::get('plugin_loader')->getPlugin('Geolocation')->getIniVersion();
queue_css_file('leaflet/leaflet', null, null, 'javascripts', $version);
queue_css_file('leaflet-draw/leaflet.draw', null, null, 'javascripts', $version);
- queue_css_file('geolocation-marker', null, null, 'css', $version);
- queue_js_file(['leaflet/leaflet', 'leaflet/leaflet-providers', 'leaflet-draw/leaflet.draw', 'map'], 'javascripts', [], $version);
-
- if (get_option('geolocation_cluster')) {
- queue_css_file(['MarkerCluster', 'MarkerCluster.Default'], null, null, 'javascripts/leaflet-markercluster', $version);
- queue_js_file('leaflet-markercluster/leaflet.markercluster', 'javascripts', [], $version);
- }
+ queue_css_file('geolocation-map', null, null, 'css', $version);
+ queue_css_file(['MarkerCluster', 'MarkerCluster.Default'], null, null, 'javascripts/leaflet-markercluster', $version);
+ queue_js_file(['leaflet/leaflet', 'leaflet/leaflet-providers', 'leaflet-draw/leaflet.draw', 'leaflet-deflate/L.Deflate', 'leaflet-markercluster/leaflet.markercluster', 'map'], 'javascripts', [], $version);
}
public function hookAfterSaveItem($args)
@@ -281,7 +285,7 @@ public function hookAfterSaveItem($args)
$item = $args['record'];
// geolocation_form_shown is a sentinel set by input-partial.php. Its
// presence means the map form was rendered, so an empty geolocation_locations
- // value means all markers were deleted, not that the form was absent.
+ // value means all locations were deleted, not that the form was absent.
if (!isset($post['geolocation_form_shown'])) {
return;
}
@@ -295,7 +299,7 @@ public function hookAfterSaveItem($args)
}
foreach (json_decode($post['geolocation_locations'] ?? '[]', true) as $entry) {
- if (!is_numeric($entry['latitude'] ?? null) || !is_numeric($entry['longitude'] ?? null)) {
+ if (empty($entry['geometry_json'])) {
continue;
}
$id = !empty($entry['id']) ? (int) $entry['id'] : null;
@@ -648,9 +652,9 @@ public function geolocationShortcode($args)
$options = [];
if (isset($args['fit'])) {
- $options['fitMarkers'] = $booleanFilter->filter($args['fit']);
+ $options['fitLocations'] = $booleanFilter->filter($args['fit']);
} else {
- $options['fitMarkers'] = '1';
+ $options['fitLocations'] = '1';
}
if (isset($args['type'])) {
@@ -700,27 +704,29 @@ protected function _mapForm($item, $label = '', $view = null)
$existingLocations = [];
if (isset($_POST['geolocation_form_shown'])) {
foreach (json_decode($_POST['geolocation_locations'] ?? '[]', true) as $entry) {
- if (!is_numeric($entry['latitude'] ?? null) || !is_numeric($entry['longitude'] ?? null)) {
+ if (empty($entry['geometry_json'])) {
continue;
}
$existingLocations[] = [
- 'id' => !empty($entry['id']) ? (int) $entry['id'] : null,
- 'latitude' => (float) $entry['latitude'],
- 'longitude' => (float) $entry['longitude'],
- 'zoom_level' => (int) ($entry['zoom_level'] ?? 0),
- 'address' => $entry['address'] ?? '',
- 'label' => $entry['label'] ?? '',
+ 'id' => !empty($entry['id']) ? (int) $entry['id'] : null,
+ 'latitude' => (float) ($entry['latitude'] ?? 0),
+ 'longitude' => (float) ($entry['longitude'] ?? 0),
+ 'zoom_level' => (int) ($entry['zoom_level'] ?? 0),
+ 'address' => $entry['address'] ?? '',
+ 'label' => $entry['label'] ?? '',
+ 'geometry_json' => $entry['geometry_json'],
];
}
} elseif ($item && $item->id) {
foreach ($this->_db->getTable('Location')->findBy(['item_id' => $item->id]) as $loc) {
$existingLocations[] = [
- 'id' => $loc->id,
- 'latitude' => $loc->latitude,
- 'longitude' => $loc->longitude,
- 'zoom_level' => $loc->zoom_level,
- 'address' => $loc->address,
- 'label' => $loc->label,
+ 'id' => $loc->id,
+ 'latitude' => $loc->latitude,
+ 'longitude' => $loc->longitude,
+ 'zoom_level' => $loc->zoom_level,
+ 'address' => $loc->address,
+ 'label' => $loc->label,
+ 'geometry_json' => $loc->geometry_json,
];
}
}
@@ -803,7 +809,7 @@ public function filterStaticSiteExportOmekaShortcodeCallbacks($callbacks)
// @see GeolocationPlugin::geolocationShortcode()
$callbacks['geolocation'] = function ($args, $frontMatter, $job) {
$frontMatter['css'][] = 'vendor/leaflet/leaflet.css';
- $frontMatter['css'][] = 'vendor/omeka-geolocation/geolocation-marker.css';
+ $frontMatter['css'][] = 'vendor/omeka-geolocation/geolocation-map.css';
$frontMatter['js'][] = 'vendor/jquery/jquery.js';
$frontMatter['js'][] = 'vendor/leaflet/leaflet.js';
$frontMatter['js'][] = 'vendor/omeka-geolocation/geolocation-locations.js';
@@ -825,7 +831,7 @@ public function hookStaticSiteExportSiteExportPost($args)
'title' => __('Map'),
'css' => [
'vendor/leaflet/leaflet.css',
- 'vendor/omeka-geolocation/geolocation-marker.css',
+ 'vendor/omeka-geolocation/geolocation-map.css',
],
'js' => [
'vendor/jquery/jquery.js',
@@ -869,7 +875,7 @@ public function hookStaticSiteExportItemBundle($args)
}
$frontMatterPage['css'][] = 'vendor/leaflet/leaflet.css';
- $frontMatterPage['css'][] = 'vendor/omeka-geolocation/geolocation-marker.css';
+ $frontMatterPage['css'][] = 'vendor/omeka-geolocation/geolocation-map.css';
$frontMatterPage['js'][] = 'vendor/jquery/jquery.js';
$frontMatterPage['js'][] = 'vendor/leaflet/leaflet.js';
$frontMatterPage['js'][] = 'vendor/omeka-geolocation/geolocation-locations.js';
@@ -916,7 +922,7 @@ public function hookExhibitBuilderStaticSiteExportExhibitPageBlock($args)
$attachments = $exhibitPageBlock->getAttachments();
$frontMatterExhibitPage['css'][] = 'vendor/leaflet/leaflet.css';
- $frontMatterExhibitPage['css'][] = 'vendor/omeka-geolocation/geolocation-marker.css';
+ $frontMatterExhibitPage['css'][] = 'vendor/omeka-geolocation/geolocation-map.css';
$frontMatterExhibitPage['js'][] = 'vendor/jquery/jquery.js';
$frontMatterExhibitPage['js'][] = 'vendor/leaflet/leaflet.js';
$frontMatterExhibitPage['js'][] = 'vendor/omeka-geolocation/geolocation-locations.js';
@@ -945,6 +951,7 @@ private function _locationToStaticSiteExportArray(Location $location, Item $item
{
$file = $item->getFile();
return [
+ 'geometry_json' => $location->geometry_json,
'latitude' => $location->latitude,
'longitude' => $location->longitude,
'zoomLevel' => $location->zoom_level,
diff --git a/config_form.php b/config_form.php
index 0aa02ac..7a34919 100644
--- a/config_form.php
+++ b/config_form.php
@@ -258,10 +258,10 @@
diff --git a/libraries/Geolocation/StaticSiteExport/omeka-geolocation/geolocation-locations.js b/libraries/Geolocation/StaticSiteExport/omeka-geolocation/geolocation-locations.js
index 02e2349..0d5b479 100644
--- a/libraries/Geolocation/StaticSiteExport/omeka-geolocation/geolocation-locations.js
+++ b/libraries/Geolocation/StaticSiteExport/omeka-geolocation/geolocation-locations.js
@@ -12,6 +12,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
const featureGroup = L.featureGroup();
// Get the locations data and add the locations to the map.
+ let lastGeometry = null;
locationsData.forEach((locationData) => {
const popupDiv = document.createElement('div');
const popupHeading = document.createElement('h2');
@@ -27,14 +28,14 @@ document.addEventListener('DOMContentLoaded', function(event) {
popupDiv.appendChild(popupImg);
}
- const marker = L.marker([locationData.latitude, locationData.longitude]);
- marker.bindPopup(popupDiv);
- marker.addTo(featureGroup);
+ lastGeometry = JSON.parse(locationData.geometry_json);
+ const layer = L.geoJSON(lastGeometry);
+ layer.bindPopup(popupDiv);
+ layer.addTo(featureGroup);
});
map.fitBounds(featureGroup.getBounds());
- if (locationsData.length === 1) {
- // Set the zoom level if there is only one location.
+ if (locationsData.length === 1 && lastGeometry.type === 'Point') {
map.setZoom(locationsData[0].zoomLevel ?? 15);
}
diff --git a/libraries/Geolocation/StaticSiteExport/omeka-geolocation/geolocation-marker.css b/libraries/Geolocation/StaticSiteExport/omeka-geolocation/geolocation-map.css
similarity index 60%
rename from libraries/Geolocation/StaticSiteExport/omeka-geolocation/geolocation-marker.css
rename to libraries/Geolocation/StaticSiteExport/omeka-geolocation/geolocation-map.css
index c413414..b50a3f8 100644
--- a/libraries/Geolocation/StaticSiteExport/omeka-geolocation/geolocation-marker.css
+++ b/libraries/Geolocation/StaticSiteExport/omeka-geolocation/geolocation-map.css
@@ -28,16 +28,33 @@ div#geolocation {
padding:0;
}
-.geolocation_balloon {
+.leaflet-popup-content-wrapper:has(.geolocation-popup) {
+ overflow: hidden;
+ padding: 0;
+}
+
+.leaflet-popup-content:has(.geolocation-popup) {
+ margin: 0;
+}
+
+.geolocation-popup {
width: 200px;
+ padding: 0 20px 13px;
}
-.geolocation_balloon img {
- max-width: 100%;
+
+.geolocation-popup-header {
+ margin: 0 -20px 13px;
+ padding: 8px 20px;
+ background: #e3e3e3;
+ font-weight: bold;
+}
+
+.geolocation-popup a {
+ border-bottom: none;
}
-.geolocation_balloon_title {
- font-weight:bold;
- font-size:18px;
- margin-bottom:0px;
+
+.geolocation-popup img {
+ max-width: 100%;
}
img.leaflet-tile,
diff --git a/models/Api/Location.php b/models/Api/Location.php
index cf6273c..b517b7a 100644
--- a/models/Api/Location.php
+++ b/models/Api/Location.php
@@ -22,6 +22,7 @@ public function getRepresentation(Omeka_Record_AbstractRecord $record)
$representation = [
'id' => $record->id,
'url' => $this->getResourceUrl("/geolocations/{$record->id}"),
+ 'geometry_json' => $record->geometry_json,
'latitude' => $record->latitude,
'longitude' => $record->longitude,
'zoom_level' => $record->zoom_level,
@@ -63,11 +64,14 @@ public function setPutData(Omeka_Record_AbstractRecord $record, $data)
private function _applyLocationFields(Omeka_Record_AbstractRecord $record, $data)
{
- if (isset($data->latitude)) {
- $record->latitude = $data->latitude;
- }
- if (isset($data->longitude)) {
- $record->longitude = $data->longitude;
+ if (isset($data->geometry_json)) {
+ $record->geometry_json = $data->geometry_json;
+ } elseif (isset($data->latitude) && isset($data->longitude)) {
+ // Fallback for pre-4.0 API clients that post lat/lng without geometry_json
+ $record->geometry_json = json_encode([
+ 'type' => 'Point',
+ 'coordinates' => [(float) $data->longitude, (float) $data->latitude],
+ ]);
}
if (isset($data->zoom_level)) {
$record->zoom_level = $data->zoom_level;
diff --git a/models/Location.php b/models/Location.php
index 9477393..219284f 100644
--- a/models/Location.php
+++ b/models/Location.php
@@ -12,6 +12,7 @@ class Location extends Omeka_Record_AbstractRecord implements Zend_Acl_Resource_
public $zoom_level;
public $address;
public $label;
+ public $geometry_json;
/**
* Executes before the record is saved.
@@ -24,6 +25,26 @@ protected function beforeSave($args)
if (is_null($this->label)) {
$this->label = '';
}
+ // latitude and longitude are kept in sync with geometry_json so that
+ // geographic radius search (hookItemsBrowseSql) works for all location
+ // types without spatial SQL functions. For shapes, we use the bounding
+ // box center as a representative point.
+ $geometry = json_decode($this->geometry_json, true);
+ if ($geometry) {
+ if ($geometry['type'] === 'Point') {
+ $this->longitude = $geometry['coordinates'][0];
+ $this->latitude = $geometry['coordinates'][1];
+ } else {
+ // Polygon coordinates[0] is the outer boundary; LineString coordinates is the points array directly
+ $coords = $geometry['type'] === 'Polygon'
+ ? $geometry['coordinates'][0]
+ : $geometry['coordinates'];
+ $lngs = array_column($coords, 0);
+ $lats = array_column($coords, 1);
+ $this->longitude = (min($lngs) + max($lngs)) / 2;
+ $this->latitude = (min($lats) + max($lats)) / 2;
+ }
+ }
}
/**
@@ -38,15 +59,47 @@ protected function _validate()
if (!$this->getTable('Item')->exists($this->item_id)) {
$this->addError('item_id', __('Location requires a valid item ID.'));
}
- if (!is_numeric($this->latitude)) {
- $this->addError('latitude', __('Location requires a latitude.'));
+ if (!$this->_isValidGeometry(json_decode($this->geometry_json, true))) {
+ $this->addError('geometry_json', __('Location requires a valid geometry.'));
+ }
+ }
+
+ /** Validates that $geometry is a well-formed GeoJSON geometry object. */
+ private function _isValidGeometry($geometry)
+ {
+ if (!is_array($geometry)) {
+ return false;
+ }
+ $type = $geometry['type'] ?? '';
+ $coords = $geometry['coordinates'] ?? null;
+ if (!is_array($coords)) {
+ return false;
+ }
+ if ($type === 'Point') {
+ return $this->_isValidPosition($coords);
}
- if (!is_numeric($this->longitude)) {
- $this->addError('longitude', __('Location requires a longitude.'));
+ if ($type === 'LineString') {
+ return count($coords) >= 2 && $this->_areValidPositions($coords);
}
- if (!is_numeric($this->zoom_level)) {
- $this->addError('zoom_level', __('Location requires a zoom level.'));
+ if ($type === 'Polygon') {
+ return isset($coords[0]) && count($coords[0]) >= 4 && $this->_areValidPositions($coords[0]);
+ }
+ return false; // unrecognized type
+ }
+
+ private function _isValidPosition($pos)
+ {
+ return is_array($pos) && count($pos) >= 2 && is_numeric($pos[0]) && is_numeric($pos[1]);
+ }
+
+ private function _areValidPositions($positions)
+ {
+ foreach ($positions as $pos) {
+ if (!$this->_isValidPosition($pos)) {
+ return false;
+ }
}
+ return true;
}
/**
diff --git a/views/helpers/GeolocationMapBrowse.php b/views/helpers/GeolocationMapBrowse.php
index ccc16a5..41455ec 100644
--- a/views/helpers/GeolocationMapBrowse.php
+++ b/views/helpers/GeolocationMapBrowse.php
@@ -22,8 +22,8 @@ public function geolocationMapBrowse($divId = 'map', $options = [], $attrs = [],
$options['uri'] = url('geolocation/map/browse-json');
}
- if (!array_key_exists('fitMarkers', $options)) {
- $options['fitMarkers'] = (bool) get_option('geolocation_auto_fit_browse');
+ if (!array_key_exists('fitLocations', $options)) {
+ $options['fitLocations'] = (bool) get_option('geolocation_auto_fit_browse');
}
$class = 'map geolocation-map';
diff --git a/views/helpers/GeolocationMapOptions.php b/views/helpers/GeolocationMapOptions.php
index c30605e..5195023 100644
--- a/views/helpers/GeolocationMapOptions.php
+++ b/views/helpers/GeolocationMapOptions.php
@@ -28,8 +28,12 @@ public function geolocationMapOptions($options = [])
$options['custom_map'] = json_decode((string) get_option('geolocation_custom_map'), true);
$options['strings'] = [
- 'fitAllMarkers' => __('Fit all markers'),
- 'label' => __('Label'),
+ 'fitAllLocations' => __('Fit all locations'),
+ 'label' => __('Label'),
+ 'editLocations' => __('Edit locations'),
+ 'noLocationsToEdit' => __('No locations to edit'),
+ 'deleteLocations' => __('Delete locations'),
+ 'noLocationsToDelete' => __('No locations to delete'),
];
return js_escape($options);
diff --git a/views/helpers/GeolocationMapSingle.php b/views/helpers/GeolocationMapSingle.php
index 8058f97..ff5ecab 100644
--- a/views/helpers/GeolocationMapSingle.php
+++ b/views/helpers/GeolocationMapSingle.php
@@ -12,7 +12,7 @@ public function geolocationMapSingle($item = null, $width = '200px', $height = '
}
// For single-location items this sets the initial zoom correctly.
- // For multi-location items fitMarkers() overrides the center after all points are added.
+ // For multi-location items fitLocations() overrides the center after all points are added.
$center = [
'latitude' => $locations[0]->latitude,
'longitude' => $locations[0]->longitude,
@@ -22,14 +22,12 @@ public function geolocationMapSingle($item = null, $width = '200px', $height = '
$points = [];
foreach ($locations as $loc) {
$point = [
- 'latitude' => $loc->latitude,
- 'longitude' => $loc->longitude,
- 'zoomLevel' => $loc->zoom_level,
- 'label' => $loc->label,
+ 'geometry_json' => $loc->geometry_json,
+ 'label' => $loc->label,
];
if ($loc->label !== '') {
- $point['markerHtml'] = ''
- . '
' . html_escape($loc->label) . '
'
+ $point['popupHtml'] = '';
}
$points[] = $point;
@@ -37,7 +35,8 @@ public function geolocationMapSingle($item = null, $width = '200px', $height = '
$options = [];
$options['basemap'] = get_option('geolocation_basemap');
- $options['points'] = $points;
+ $options['locations'] = $points;
+ $options['cluster'] = true;
$options = $this->view->geolocationMapOptions($options);
$center = js_escape($center);
$varDivId = Inflector::variablize($divId);
diff --git a/views/shared/css/geolocation-items-map.css b/views/shared/css/geolocation-items-map.css
index 5997e3f..3b9de08 100644
--- a/views/shared/css/geolocation-items-map.css
+++ b/views/shared/css/geolocation-items-map.css
@@ -41,14 +41,6 @@
margin-top: 0 !important;
}
-/* The map for the items page needs a bit of styling on it */
-#address_balloon dt {
- font-weight: bold;
-}
-#address_balloon {
- width: 100px;
-}
-
div.map-notification {
display:block;
border: 1px dotted #ccc;
diff --git a/views/shared/css/geolocation-marker.css b/views/shared/css/geolocation-map.css
similarity index 67%
rename from views/shared/css/geolocation-marker.css
rename to views/shared/css/geolocation-map.css
index 1b05bb5..02260ba 100644
--- a/views/shared/css/geolocation-marker.css
+++ b/views/shared/css/geolocation-map.css
@@ -1,6 +1,6 @@
#omeka-map-form {
width: 100%;
- height: 300px;
+ height: 500px;
clear: both;
}
#geolocation_address {
@@ -28,16 +28,33 @@ div#geolocation {
padding:0;
}
-.geolocation_balloon {
+.leaflet-popup-content-wrapper:has(.geolocation-popup) {
+ overflow: hidden;
+ padding: 0;
+}
+
+.leaflet-popup-content:has(.geolocation-popup) {
+ margin: 0;
+}
+
+.geolocation-popup {
width: 200px;
+ padding: 0 20px 13px;
}
-.geolocation_balloon img {
- max-width: 100%;
+
+.geolocation-popup-header {
+ margin: 0 -20px 13px;
+ padding: 8px 20px;
+ background: #e3e3e3;
+ font-weight: bold;
+}
+
+.geolocation-popup a {
+ border-bottom: none;
}
-.geolocation_balloon_title {
- font-weight:bold;
- font-size:18px;
- margin-bottom:0px;
+
+.geolocation-popup img {
+ max-width: 100%;
}
.leaflet-control-fit-all a {
diff --git a/views/shared/exhibit_layouts/geolocation-map/layout.php b/views/shared/exhibit_layouts/geolocation-map/layout.php
index 85a0a7d..15be75f 100644
--- a/views/shared/exhibit_layouts/geolocation-map/layout.php
+++ b/views/shared/exhibit_layouts/geolocation-map/layout.php
@@ -10,6 +10,7 @@
foreach ($attachments as $attachment):
$item = $attachment->getItem();
$file = $attachment->getFile();
+ $title = metadata($item, 'display_title', ['no_escape' => true]);
$titleLink = exhibit_builder_link_to_exhibit_item(null, [], $item);
if ($file):
@@ -20,14 +21,14 @@
$itemLocations = $locationTable->findBy(['item_id' => $item->id]);
foreach ($itemLocations as $location):
- $title = $titleLink . ($location->label ? ' — ' . html_escape($location->label) : '');
- $html = '
'
- . '
' . $title . '
'
+ $headerText = $location->label ? html_escape($location->label) : html_escape($title);
+ $html = '';
$locations[] = [
- 'lat' => $location->latitude,
- 'lng' => $location->longitude,
+ 'geometry_json' => $location->geometry_json,
'html' => $html,
];
endforeach;
@@ -43,13 +44,9 @@
var map_locations = ;
for (var i = 0; i < map_locations.length; i++) {
var locationData = map_locations[i];
- geolocation_map.addMarker(
- [locationData.lat, locationData.lng],
- {},
- locationData.html
- );
+ geolocation_map.addLayerFromGeometry(JSON.parse(locationData.geometry_json), {}, locationData.html);
}
- geolocation_map.fitMarkers();
+ geolocation_map.fitLocations();
});
diff --git a/views/shared/javascripts/leaflet-deflate/L.Deflate.js b/views/shared/javascripts/leaflet-deflate/L.Deflate.js
new file mode 100644
index 0000000..09782e0
--- /dev/null
+++ b/views/shared/javascripts/leaflet-deflate/L.Deflate.js
@@ -0,0 +1 @@
+"use strict";L.Layer.include({_originalRemove:L.Layer.prototype.remove,remove:function(){if(this.marker){this.marker.remove()}return this._originalRemove()}});L.Map.include({_originalRemoveLayer:L.Map.prototype.removeLayer,removeLayer:function(layer){if(layer.marker){layer.marker.remove()}return this._originalRemoveLayer(layer)}});L.Deflate=L.FeatureGroup.extend({options:{minSize:10,markerOptions:{},markerType:L.marker,greedyCollapse:true},initialize:function(options){L.Util.setOptions(this,options);this._layers=[];this._needsPrepping=[];this._featureLayer=this._getFeatureLayer(options)},_getFeatureLayer:function(){if(this.options.markerLayer){return this.options.markerLayer}return L.featureGroup(this.options)},_getBounds:function(path){if(path instanceof L.Circle){path.addTo(this._map);const bounds=path.getBounds();this._map.removeLayer(path);return bounds}return path.getBounds()},_isCollapsed:function(path,zoom){const bounds=path.computedBounds;const northEastPixels=this._map.project(bounds.getNorthEast(),zoom);const southWestPixels=this._map.project(bounds.getSouthWest(),zoom);const width=Math.abs(northEastPixels.x-southWestPixels.x);const height=Math.abs(southWestPixels.y-northEastPixels.y);if(this.options.greedyCollapse){return height
0) {
- this.map.fitBounds(this.markerBounds, {padding: [25, 25]});
+ fitLocations: function () {
+ if (!this.locationBounds.isValid()) {
+ return;
+ }
+ var bounds = this.locationBounds;
+ // fitBounds on a zero-area bounds (single point) zooms in too
+ // aggressively; panTo preserves the set zoom level.
+ if (bounds.getNorth() === bounds.getSouth() && bounds.getEast() === bounds.getWest()) {
+ this.map.panTo(bounds.getCenter());
+ } else {
+ this.map.fitBounds(bounds, {padding: [25, 25]});
}
},
+ addShapeLayer: function (geojson, bindHtml) {
+ var layer = L.GeoJSON.geometryToLayer(geojson);
+ if (bindHtml) {
+ layer.bindPopup(bindHtml, {autoPanPadding: [50, 50]});
+ }
+ this.deflateGroup.addLayer(layer);
+ this.locationBounds.extend(layer.getBounds());
+ return layer;
+ },
+
+ addLayerFromGeometry: function (geometry, options, bindHtml) {
+ var layer;
+ if (geometry.type === 'Point') {
+ layer = this.addMarker([geometry.coordinates[1], geometry.coordinates[0]], options, bindHtml);
+ } else {
+ layer = this.addShapeLayer(geometry, bindHtml);
+ }
+ if (bindHtml) {
+ var srAlertsDiv = jQuery('#geolocation-sr-alerts');
+ var title = options.title || '';
+ var latlng = geometry.type === 'Point'
+ ? layer.getLatLng()
+ : layer.getBounds().getCenter();
+ var parts = [title, srAlertsDiv.data('latString'), latlng.lat,
+ srAlertsDiv.data('longString'), latlng.lng];
+ var srOpenedText = parts.concat(srAlertsDiv.data('openedString')).join(' ');
+ var srClosedText = parts.concat(srAlertsDiv.data('closedString')).join(' ');
+ // Leaflet popup events give no screen reader feedback; announce the
+ // layer title, coordinates, and open/close status.
+ layer.addEventListener('popupopen', function () {
+ srAlertsDiv.text(srOpenedText);
+ });
+ layer.addEventListener('popupclose', function () {
+ srAlertsDiv.text(srClosedText);
+ });
+ // Popup dimensions are calculated before images load; update on
+ // first open so the popup resizes correctly once the image loads.
+ layer.once('popupopen', function (event) {
+ var popup = event.popup;
+ jQuery(popup.getElement()).find('img').one('load', function () {
+ popup.update();
+ });
+ });
+ }
+ return layer;
+ },
+
initMap: function () {
var customMap = this.options.custom_map;
@@ -81,7 +95,8 @@ OmekaMap.prototype = {
}
this.map = L.map(this.mapDivId).setView([this.center.latitude, this.center.longitude], this.center.zoomLevel);
- this.markerBounds = L.latLngBounds();
+ this.locationBounds = L.latLngBounds();
+ this.markers = [];
L.tileLayer.provider(this.options.basemap, this.options.basemapOptions).addTo(this.map);
@@ -100,9 +115,17 @@ OmekaMap.prototype = {
this.clusterGroup = L.markerClusterGroup({
showCoverageOnHover: false
});
- this.map.addLayer(this.clusterGroup);
}
+ // markerLayer routes collapsed shapes into the cluster group so
+ // they cluster alongside point markers.
+ this.deflateGroup = L.deflate({
+ minSize: 10,
+ markerLayer: this.clusterGroup,
+ greedyCollapse: false,
+ });
+ this.map.addLayer(this.deflateGroup);
+
jQuery(this.map.getContainer()).trigger('o:geolocation:init_map', this);
new OmekaFitControl({ position: 'topleft', omekaMap: this }).addTo(this.map);
@@ -115,14 +138,14 @@ var OmekaFitControl = L.Control.extend({
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-fit-all');
var link = L.DomUtil.create('a', '', container);
link.href = '#';
- link.title = omekaMap.options.strings.fitAllMarkers;
+ link.title = omekaMap.options.strings.fitAllLocations;
link.setAttribute('role', 'button');
- link.setAttribute('aria-label', omekaMap.options.strings.fitAllMarkers);
+ link.setAttribute('aria-label', omekaMap.options.strings.fitAllLocations);
link.innerHTML = '';
L.DomEvent.on(link, 'click', function (e) {
L.DomEvent.preventDefault(e);
L.DomEvent.stopPropagation(e);
- omekaMap.fitMarkers();
+ omekaMap.fitLocations();
});
this._link = link;
return container;
@@ -142,8 +165,8 @@ function OmekaMapBrowse(mapDivId, center, options) {
OmekaMapBrowse.prototype = {
afterLoadItems: function () {
- if (this.options.fitMarkers) {
- this.fitMarkers();
+ if (this.options.fitLocations) {
+ this.fitLocations();
}
if (!this.options.list) {
@@ -177,26 +200,31 @@ OmekaMapBrowse.prototype = {
},
buildLayerFromLocation: function (locationData) {
- this.addMarker(
- [locationData.latitude, locationData.longitude],
- {title: locationData.title, alt: locationData.title},
- this.buildMarkerContent(locationData)
- );
+ var geometry = JSON.parse(locationData.geometry_json);
+ var layer = this.addLayerFromGeometry(geometry, {title: locationData.title, alt: locationData.title}, this.buildLocationContent(locationData));
+ // Shapes have no native title property; _geolocationTitle is read
+ // by buildListLinks to label them in the sidebar. Points are omitted
+ // because they carry their title via marker.options.title.
+ if (geometry.type !== 'Point') {
+ layer._geolocationTitle = locationData.title || '';
+ }
},
- buildMarkerContent: function (locationData) {
- var balloon = jQuery('