diff --git a/GeolocationPlugin.php b/GeolocationPlugin.php
index 3a4a30a..b841c77 100644
--- a/GeolocationPlugin.php
+++ b/GeolocationPlugin.php
@@ -34,8 +34,6 @@ class GeolocationPlugin extends Omeka_Plugin_AbstractPlugin
protected $_filters = [
'admin_navigation_main',
'public_navigation_main',
- 'response_contexts',
- 'action_contexts',
'admin_items_form_tabs',
'public_navigation_items',
'api_resources',
@@ -58,8 +56,9 @@ public function hookInstall()
`latitude` DOUBLE NOT NULL ,
`longitude` DOUBLE NOT NULL ,
`zoom_level` INT NOT NULL ,
- `map_type` VARCHAR( 255 ) 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);
@@ -73,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,6 +163,16 @@ public function hookUpgrade($args)
set_option('geolocation_basemap', $stamenBasemaps[$currentBasemap]);
}
}
+ if (version_compare($args['old_version'], '4.0', '<')) {
+ $db = get_db();
+ // 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");
+ }
}
/**
@@ -244,15 +254,6 @@ public function hookDefineRoutes($args)
]);
$router->addRoute('items_map', $mapRoute);
- // Trying to make the route look like a KML file so google will eat it.
- // @todo Include page parameter if this works.
- $kmlRoute = new Zend_Controller_Router_Route_Regex('geolocation/map\.kml', [
- 'controller' => 'map',
- 'action' => 'browse',
- 'module' => 'geolocation',
- 'output' => 'kml',
- ]);
- $router->addRoute('map_kml', $kmlRoute);
}
public function hookAdminHead($args)
@@ -267,17 +268,20 @@ public function hookPublicHead($args)
private function _head()
{
- $pluginLoader = Zend_Registry::get('plugin_loader');
- $geolocation = $pluginLoader->getPlugin('Geolocation');
- $version = $geolocation->getIniVersion();
+ $version = Zend_Registry::get('plugin_loader')->getPlugin('Geolocation')->getIniVersion();
queue_css_file('leaflet/leaflet', null, null, 'javascripts', $version);
- queue_css_file('geolocation-marker', null, null, 'css', $version);
- queue_js_file(['leaflet/leaflet', 'leaflet/leaflet-providers', 'map'], 'javascripts', [], $version);
+ queue_css_file('leaflet-draw/leaflet.draw', null, null, 'javascripts', $version);
+ queue_css_file('geolocation-map', null, null, 'css', $version);
+ // Marker clustering is optional, so its assets load only when enabled.
+ // leaflet-deflate always loads because every map initializes it.
+ $jsFiles = ['leaflet/leaflet', 'leaflet/leaflet-providers', 'leaflet-draw/leaflet.draw', 'leaflet-deflate/L.Deflate'];
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);
+ $jsFiles[] = 'leaflet-markercluster/leaflet.markercluster';
}
+ $jsFiles[] = 'map';
+ queue_js_file($jsFiles, 'javascripts', [], $version);
}
public function hookAfterSaveItem($args)
@@ -287,32 +291,42 @@ public function hookAfterSaveItem($args)
}
$item = $args['record'];
- // If we don't have the geolocation form on the page, don't do anything!
- if (!isset($post['geolocation'])) {
+ // 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 locations were deleted, not that the form was absent.
+ if (!isset($post['geolocation_form_shown'])) {
return;
}
- // Find the location object for the item
- $location = $this->_db->getTable('Location')->findLocationByItem($item, true);
+ // Build an index of existing locations. As POST entries are matched to existing
+ // records, they are removed from $remaining. Whatever is left at the end was
+ // not in the POST and gets deleted.
+ $remaining = [];
+ foreach ($this->_db->getTable('Location')->findBy(['item_id' => $item->id]) as $loc) {
+ $remaining[$loc->id] = $loc;
+ }
- // If we have filled out info for the geolocation, then submit to the db
- $geolocationPost = $post['geolocation'];
- if (!empty($geolocationPost)
- && $geolocationPost['latitude'] != ''
- && $geolocationPost['longitude'] != ''
- ) {
- if (!$location) {
+ foreach (json_decode($post['geolocation_locations'] ?? '[]', true) as $entry) {
+ if (empty($entry['geometry_json'])) {
+ continue;
+ }
+ $id = !empty($entry['id']) ? (int) $entry['id'] : null;
+ if ($id && isset($remaining[$id])) {
+ $location = $remaining[$id];
+ unset($remaining[$id]);
+ } else {
$location = new Location;
$location->item_id = $item->id;
}
- $location->setPostData($geolocationPost);
+ // Exclude 'id' so a crafted POST cannot cause setPostData to set
+ // the id on a new record, which would trigger an UPDATE on a row
+ // belonging to a different item.
+ $location->setPostData(array_diff_key($entry, ['id' => null]));
$location->save();
- } else {
- // If the form is empty, then we want to delete whatever location is
- // currently stored
- if ($location) {
- $location->delete();
- }
+ }
+
+ foreach ($remaining as $loc) {
+ $loc->delete();
}
}
@@ -320,9 +334,9 @@ public function hookAdminItemsShowSidebar($args)
{
$view = $args['view'];
$item = $args['item'];
- $location = $this->_db->getTable('Location')->findLocationByItem($item, true);
+ $locations = $this->_db->getTable('Location')->findBy(['item_id' => $item->id]);
- if ($location) {
+ if ($locations) {
$html = ''
. '
'
. '
' . __('Geolocation') . ' '
@@ -341,9 +355,9 @@ public function hookPublicItemsShow($args)
$view = $args['view'];
$item = $args['item'];
- $location = $this->_db->getTable('Location')->findLocationByItem($item, true);
+ $locations = $this->_db->getTable('Location')->findBy(['item_id' => $item->id]);
- if ($location) {
+ if ($locations) {
$width = $this->_filterCssLength(get_option('geolocation_item_map_width'), '100%');
$height = $this->_filterCssLength(get_option('geolocation_item_map_height'), '300px');
$html = "
";
@@ -518,22 +532,6 @@ public function filterPublicNavigationMain($navArray)
return $navArray;
}
- public function filterResponseContexts($contexts)
- {
- $contexts['kml'] = ['suffix' => 'kml',
- 'headers' => ['Content-Type' => 'text/xml']];
- return $contexts;
- }
-
- public function filterActionContexts($contexts, $args)
- {
- $controller = $args['controller'];
- if ($controller instanceof Geolocation_MapController) {
- $contexts['browse'] = ['kml'];
- }
- return $contexts;
- }
-
public function filterAdminItemsFormTabs($tabs, $args)
{
// insert the map tab before the Miscellaneous tab
@@ -563,8 +561,11 @@ public function filterPublicNavigationItems($navArray)
public function filterApiResources($apiResources)
{
$apiResources['geolocations'] = [
- 'record_type' => 'Location',
- 'actions' => ['get', 'index', 'post', 'put', 'delete'],
+ 'record_type' => 'Location',
+ 'actions' => ['get', 'index', 'post', 'put', 'delete'],
+ // Whitelist item_id as an allowed GET param on the index action;
+ // without this the API router rejects ?item_id=X requests.
+ 'index_params' => ['item_id'],
];
return $apiResources;
}
@@ -579,14 +580,15 @@ public function filterApiResources($apiResources)
public function filterApiExtendItems($extend, $args)
{
$item = $args['record'];
- $location = $this->_db->getTable('Location')->findBy(['item_id' => $item->id]);
- if (!$location) {
+ $locations = $this->_db->getTable('Location')->findBy(['item_id' => $item->id]);
+ if (!$locations) {
return $extend;
}
- $locationId = $location[0]['id'];
+ // count+url is the Omeka API format for multi-resource references;
+ // ApiController validates this shape and rejects plain arrays of objects.
$extend['geolocations'] = [
- 'id' => $locationId,
- 'url' => Omeka_Record_Api_AbstractRecordAdapter::getResourceUrl("/geolocations/$locationId"),
+ 'count' => count($locations),
+ 'url' => Omeka_Record_Api_AbstractRecordAdapter::getResourceUrl('/geolocations') . '?item_id=' . $item->id,
'resource' => 'geolocations',
];
return $extend;
@@ -602,7 +604,7 @@ public function hookContributionTypeForm($args)
if (get_option('geolocation_add_map_to_contribution_form')) {
$contributionType = $args['type'];
$view = $args['view'];
- echo $this->_mapForm(null, __('Find A Geographic Location For The %s:', $contributionType->display_name), false, $view, null);
+ echo $this->_mapForm(null, __('Find A Geographic Location For The %s:', $contributionType->display_name), $view);
}
}
@@ -658,9 +660,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'])) {
@@ -692,89 +694,77 @@ public function geolocationShortcode($args)
* @param Item $item
* @param string $label if empty string, a default string will be used. Set
* null if you don't want a label.
- * @param boolean $confirmLocationChange
* @param Omeka_View $view
- * @param array $post
* @return string Html string.
*/
- protected function _mapForm($item, $label = '', $confirmLocationChange = true, $view = null, $post = null)
+ protected function _mapForm($item, $label = '', $view = null)
{
- $html = '';
-
if (is_null($view)) {
$view = get_view();
}
- // Need to be translated.
if ($label == '') {
$label = __('Find a Location by Address:');
}
- $center = $this->_getCenter();
- $center['show'] = false;
-
- $location = $this->_db->getTable('Location')->findLocationByItem($item, true);
-
- if (is_null($post)) {
- $post = $_POST;
- }
- $usePost = !empty($post)
- && !empty($post['geolocation'])
- && $post['geolocation']['longitude'] != ''
- && $post['geolocation']['latitude'] != '';
- if ($usePost) {
- $lng = empty($post['geolocation']['longitude']) ? '' : (float) $post['geolocation']['longitude'];
- $lat = empty($post['geolocation']['latitude']) ? '' : (float) $post['geolocation']['latitude'];
- $zoom = empty($post['geolocation']['zoom_level']) ? '' : (int) $post['geolocation']['zoom_level'];
- $address = html_escape($post['geolocation']['address']);
- } else {
- if ($location) {
- $lng = (float) $location['longitude'];
- $lat = (float) $location['latitude'];
- $zoom = (int) $location['zoom_level'];
- $address = html_escape($location['address']);
- } else {
- $lng = $lat = $zoom = $address = '';
+ // If the form was previously submitted (e.g. save failed validation),
+ // re-populate from POST so unsaved changes are not lost.
+ $existingLocations = [];
+ if (isset($_POST['geolocation_form_shown'])) {
+ foreach (json_decode($_POST['geolocation_locations'] ?? '[]', true) as $entry) {
+ if (empty($entry['geometry_json'])) {
+ continue;
+ }
+ $existingLocations[] = [
+ '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,
+ 'geometry_json' => $loc->geometry_json,
+ ];
}
}
- // Prepare javascript.
- $options = [];
- $options['form'] = [
- 'id' => 'location_form',
- 'posted' => $usePost,
+ // For no-location items this sets the default center.
+ // For single-location items this sets the initial zoom correctly.
+ // For multi-location items fitBounds overrides the center on tab select.
+ $center = [
+ 'latitude' => (float) get_option('geolocation_default_latitude'),
+ 'longitude' => (float) get_option('geolocation_default_longitude'),
+ 'zoomLevel' => (float) get_option('geolocation_default_zoom_level'),
];
- if ($location or $usePost) {
- $options['point'] = [
- 'latitude' => $lat,
- 'longitude' => $lng,
- 'zoomLevel' => $zoom,
- ];
- $center = $options['point'];
+ if ($existingLocations) {
+ $center['latitude'] = $existingLocations[0]['latitude'];
+ $center['longitude'] = $existingLocations[0]['longitude'];
+ $center['zoomLevel'] = $existingLocations[0]['zoom_level'];
}
- $options['confirmLocationChange'] = $confirmLocationChange;
+
+ $options = [];
+ $options['form'] = ['id' => 'location_form'];
$options['cluster'] = false;
return $view->partial('map/input-partial.php', [
'label' => $label,
- 'address' => $address,
'center' => $center,
'options' => $options,
- 'lng' => $lng,
- 'lat' => $lat,
- 'zoom' => $zoom,
+ 'existingLocations' => $existingLocations,
]);
}
- protected function _getCenter()
- {
- return [
- 'latitude' => (float) get_option('geolocation_default_latitude'),
- 'longitude' => (float) get_option('geolocation_default_longitude'),
- 'zoomLevel' => (float) get_option('geolocation_default_zoom_level'),
- ];
- }
-
protected function _filterCssLength($length, $default)
{
$length = trim((string) $length);
@@ -827,7 +817,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';
@@ -849,7 +839,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',
@@ -872,17 +862,7 @@ public function hookStaticSiteExportSiteExportPost($args)
$locations = [];
foreach ($locationRows as $locationRow) {
$item = get_db()->getTable('Item')->find($locationRow->item_id);
- $locations[] = [
- 'latitude' => $locationRow->latitude,
- 'longitude' => $locationRow->longitude,
- 'zoomLevel' => $locationRow->zoom_level,
- 'mapType' => $locationRow->map_type,
- 'address' => $locationRow->address,
- 'itemID' => $item->id,
- 'itemTitle' => $item->getDisplayTitle(),
- 'fileID' => $item->getFile() ? $item->getFile()->id : null,
- 'hasThumbnail' => $item->hasThumbnail(),
- ];
+ $locations[] = $this->_locationToStaticSiteExportArray($locationRow, $item);
}
$job->makeFile('content/geolocation/geolocation_locations.json', json_encode($locations));
}
@@ -897,29 +877,22 @@ public function hookStaticSiteExportItemBundle($args)
$frontMatterPage = $args['front_matter_page'];
$blocks = $args['blocks'];
- $location = get_db()->getTable('Location')->findLocationByItem($item, true);
- if (!$location) {
+ $itemLocations = get_db()->getTable('Location')->findBy(['item_id' => $item->id]);
+ if (!$itemLocations) {
return;
}
$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';
// Make the locations file.
- $locations = [[
- 'latitude' => $location->latitude,
- 'longitude' => $location->longitude,
- 'zoomLevel' => $location->zoom_level,
- 'mapType' => $location->map_type,
- 'address' => $location->address,
- 'itemID' => $item->id,
- 'itemTitle' => $item->getDisplayTitle(),
- 'fileID' => $item->getFile() ? $item->getFile()->id : null,
- 'hasThumbnail' => $item->hasThumbnail(),
- ]];
+ $locations = [];
+ foreach ($itemLocations as $location) {
+ $locations[] = $this->_locationToStaticSiteExportArray($location, $item);
+ }
$job->makeFile(
sprintf('content/items/%s/geolocation_locations.json', $item->id),
json_encode($locations)
@@ -957,7 +930,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';
@@ -965,21 +938,10 @@ public function hookExhibitBuilderStaticSiteExportExhibitPageBlock($args)
$locations = [];
foreach ($attachments as $attachment) {
$item = $attachment->getItem();
- $location = get_db()->getTable('Location')->findLocationByItem($item, true);
- if (!$location) {
- continue;
+ $itemLocations = get_db()->getTable('Location')->findBy(['item_id' => $item->id]);
+ foreach ($itemLocations as $location) {
+ $locations[] = $this->_locationToStaticSiteExportArray($location, $item);
}
- $locations[] = [
- 'latitude' => $location->latitude,
- 'longitude' => $location->longitude,
- 'zoomLevel' => $location->zoom_level,
- 'mapType' => $location->map_type,
- 'address' => $location->address,
- 'itemID' => $item->id,
- 'itemTitle' => $item->getDisplayTitle(),
- 'fileID' => $item->getFile() ? $item->getFile()->id : null,
- 'hasThumbnail' => $item->hasThumbnail(),
- ];
}
$job->makeFile(
sprintf('content/exhibits/%s/%s/geolocation_locations.json', $exhibit->slug, $exhibitPage->slug),
@@ -992,4 +954,21 @@ public function hookExhibitBuilderStaticSiteExportExhibitPageBlock($args)
$exhibitPage->slug
);
}
+
+ 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,
+ 'address' => $location->address,
+ 'label' => $location->label,
+ 'itemID' => $item->id,
+ 'itemTitle' => $item->getDisplayTitle(),
+ 'fileID' => $file ? $file->id : null,
+ 'hasThumbnail' => $item->hasThumbnail(),
+ ];
+ }
}
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/controllers/MapController.php b/controllers/MapController.php
index 3560f81..16dc2ef 100644
--- a/controllers/MapController.php
+++ b/controllers/MapController.php
@@ -9,30 +9,34 @@ public function init()
public function browseAction()
{
- $table = $this->_helper->db->getTable();
- $locationTable = $this->_helper->db->getTable('Location');
+ [$params, $limit, $currentPage] = $this->_getBrowseParams();
+ $this->view->totalItems = $this->_helper->db->getTable()->count($params);
+ $this->view->params = $params;
+
+ Zend_Registry::set('pagination', [
+ 'page' => $currentPage,
+ 'per_page' => $limit,
+ 'total_results' => $this->view->totalItems,
+ ]);
+ }
+
+ public function browseJsonAction()
+ {
+ [$params, $limit, $currentPage] = $this->_getBrowseParams();
+
+ $items = $this->_helper->db->getTable()->findBy($params, $limit, $currentPage);
+ $this->view->items = $items;
+ $this->view->locations = $this->_helper->db->getTable('Location')->findLocationsByItem($items);
+ $this->getResponse()->setHeader('Content-Type', 'application/json');
+ }
+
+ private function _getBrowseParams()
+ {
$params = $this->getAllParams();
$params['geolocation-mapped'] = true;
$limit = (int) get_option('geolocation_per_page');
$currentPage = $this->getParam('page', 1);
-
- // Only get pagination data for the "normal" page, only get
- // item/location data for the KML output.
- if ($this->_helper->contextSwitch->getCurrentContext() == 'kml') {
- $items = $table->findBy($params, $limit, $currentPage);
- $this->view->items = $items;
- $this->view->locations = $locationTable->findLocationByItem($items);
- } else {
- $this->view->totalItems = $table->count($params);
- $this->view->params = $params;
-
- $pagination = [
- 'page' => $currentPage,
- 'per_page' => $limit,
- 'total_results' => $this->view->totalItems,
- ];
- Zend_Registry::set('pagination', $pagination);
- }
+ return [$params, $limit, $currentPage];
}
}
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 7063412..b517b7a 100644
--- a/models/Api/Location.php
+++ b/models/Api/Location.php
@@ -22,11 +22,12 @@ 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,
- 'map_type' => $record->map_type,
'address' => $record->address,
+ 'label' => $record->label,
'item' => [
'id' => $record->item_id,
'url' => $this->getResourceUrl("/items/{$record->item_id}"),
@@ -47,25 +48,7 @@ public function setPostData(Omeka_Record_AbstractRecord $record, $data)
if (isset($data->item->id)) {
$record->item_id = $data->item->id;
}
- if (isset($data->latitude)) {
- $record->latitude = $data->latitude;
- }
- if (isset($data->longitude)) {
- $record->longitude = $data->longitude;
- }
- if (isset($data->zoom_level)) {
- $record->zoom_level = $data->zoom_level;
- }
- if (isset($data->map_type)) {
- $record->map_type = $data->map_type;
- } else {
- $record->map_type = '';
- }
- if (isset($data->address)) {
- $record->address = $data->address;
- } else {
- $record->address = '';
- }
+ $this->_applyLocationFields($record, $data);
}
/**
@@ -76,24 +59,32 @@ public function setPostData(Omeka_Record_AbstractRecord $record, $data)
*/
public function setPutData(Omeka_Record_AbstractRecord $record, $data)
{
- if (isset($data->latitude)) {
- $record->latitude = $data->latitude;
- }
- if (isset($data->longitude)) {
- $record->longitude = $data->longitude;
+ $this->_applyLocationFields($record, $data);
+ }
+
+ private function _applyLocationFields(Omeka_Record_AbstractRecord $record, $data)
+ {
+ 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;
}
- if (isset($data->map_type)) {
- $record->map_type = $data->map_type;
- } else {
- $record->map_type = '';
- }
if (isset($data->address)) {
$record->address = $data->address;
} else {
$record->address = '';
}
+ if (isset($data->label)) {
+ $record->label = $data->label;
+ } else {
+ $record->label = '';
+ }
}
}
diff --git a/models/Location.php b/models/Location.php
index 32c2c7f..219284f 100644
--- a/models/Location.php
+++ b/models/Location.php
@@ -10,20 +10,41 @@ class Location extends Omeka_Record_AbstractRecord implements Zend_Acl_Resource_
public $latitude;
public $longitude;
public $zoom_level;
- public $map_type;
public $address;
+ public $label;
+ public $geometry_json;
/**
* Executes before the record is saved.
*/
protected function beforeSave($args)
{
- if (is_null($this->map_type)) {
- $this->map_type = '';
- }
if (is_null($this->address)) {
$this->address = '';
}
+ 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,20 +59,47 @@ protected function _validate()
if (!$this->getTable('Item')->exists($this->item_id)) {
$this->addError('item_id', __('Location requires a valid item ID.'));
}
- // An item can only have one location. This assumes that updating an
- // existing location will never modify the item ID.
- if (!$this->exists() && $this->getTable()->findBy(['item_id' => $this->item_id])) {
- $this->addError('latitude', __('A location already exists for the provided item.'));
+ 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 (empty($this->latitude)) {
- $this->addError('latitude', __('Location requires a latitude.'));
+ if ($type === 'Point') {
+ return $this->_isValidPosition($coords);
}
- if (empty($this->longitude)) {
- $this->addError('longitude', __('Location requires a longitude.'));
+ if ($type === 'LineString') {
+ return count($coords) >= 2 && $this->_areValidPositions($coords);
}
- if (empty($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/models/Table/Location.php b/models/Table/Location.php
index c6d160b..f495a16 100644
--- a/models/Table/Location.php
+++ b/models/Table/Location.php
@@ -2,12 +2,12 @@
class Table_Location extends Omeka_Db_Table
{
/**
- * Returns a location (or array of locations) for an item (or array of items)
- * @param array|Item|int $item An item or item id, or an array of items or item ids
- * @param boolean $findOnlyOne Whether or not to return only one location if it exists for the item
- * @return array|Location A location or an array of locations
- **/
- public function findLocationByItem($item, $findOnlyOne = false)
+ * Returns all locations for an item or array of items, grouped by item_id.
+ *
+ * @param array|Item|int $item
+ * @return array item_id => Location[]
+ */
+ public function findLocationsByItem($item)
{
$db = get_db();
@@ -16,11 +16,10 @@ public function findLocationByItem($item, $findOnlyOne = false)
} elseif (is_array($item) && !count($item)) {
return [];
}
+
$alias = $this->getTableAlias();
- // Create a SELECT statement for the Location table
$select = $db->select()->from([$alias => $db->Location], "$alias.*");
- // Create a WHERE condition that will pull down all the location info
if (is_array($item)) {
$itemIds = [];
foreach ($item as $it) {
@@ -32,31 +31,20 @@ public function findLocationByItem($item, $findOnlyOne = false)
$select->where("$alias.item_id = ?", $itemId);
}
- // If only a single location is request, return the first one found.
- if ($findOnlyOne) {
- $location = $this->fetchObject($select);
- return $location;
- }
-
- // Get the locations.
$locations = $this->fetchObjects($select);
-
- // Return an associative array of locations where the key is the item_id of the location
- // Note: Since each item can only have one location, this makes sense to associate a single location with a single item_id.
- // However, if in the future, an item can have multiple locations, then we cannot just associate a single location with a single item_id;
- // Instead, in the future, we would have to associate an array of locations with a single item_id.
- $indexedLocations = [];
- foreach ($locations as $k => $loc) {
- $indexedLocations[$loc['item_id']] = $loc;
+ $grouped = [];
+ foreach ($locations as $loc) {
+ $grouped[$loc->item_id][] = $loc;
}
- return $indexedLocations;
+ return $grouped;
}
/**
- * Add permission check to location queries.
+ * Join items so that public permissions on items are enforced for locations.
*
- * Since all locations belong to an item we can override this method to join
- * the items table and add a permission check to the select object.
+ * Locations have no visibility of their own — a location is public only if
+ * its item is public. Joining the items table here means every query on
+ * this table automatically excludes locations for private items.
*
* @return Omeka_Db_Select
*/
diff --git a/plugin.ini b/plugin.ini
index 28c2979..f824914 100644
--- a/plugin.ini
+++ b/plugin.ini
@@ -7,4 +7,4 @@ link="https://omeka.org/classic/docs/Plugins/Geolocation/"
support_link="https://forum.omeka.org/c/omeka-classic/plugins"
omeka_minimum_version="2.5"
omeka_target_version="3.0"
-version="3.3"
+version="4.0"
diff --git a/tests/Geolocation_IntegrationHelper.php b/tests/Geolocation_IntegrationHelper.php
index 6f6b154..97b6289 100644
--- a/tests/Geolocation_IntegrationHelper.php
+++ b/tests/Geolocation_IntegrationHelper.php
@@ -38,8 +38,6 @@ public function _addPluginHooksAndFilters($pluginBroker, $pluginName)
// Add plugin filters
add_filter('admin_navigation_main', 'geolocation_admin_nav');
- add_filter('define_response_contexts', 'geolocation_kml_response_context');
- add_filter('define_action_contexts', 'geolocation_kml_action_context');
add_filter('admin_items_form_tabs', 'geolocation_item_form_tabs');
}
}
diff --git a/views/helpers/GeolocationMapBrowse.php b/views/helpers/GeolocationMapBrowse.php
index c14f572..41455ec 100644
--- a/views/helpers/GeolocationMapBrowse.php
+++ b/views/helpers/GeolocationMapBrowse.php
@@ -18,12 +18,12 @@ public function geolocationMapBrowse($divId = 'map', $options = [], $attrs = [],
if (!array_key_exists('uri', $options)) {
// This should not be a link to the public side b/c then all the URLs that
- // are generated inside the KML will also link to the public side.
- $options['uri'] = url('geolocation/map.kml');
+ // are generated inside the JSON will also link to the public side.
+ $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 1c587e3..5195023 100644
--- a/views/helpers/GeolocationMapOptions.php
+++ b/views/helpers/GeolocationMapOptions.php
@@ -27,6 +27,15 @@ public function geolocationMapOptions($options = [])
$options['custom_map'] = json_decode((string) get_option('geolocation_custom_map'), true);
+ $options['strings'] = [
+ '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 6c61812..ba4849c 100644
--- a/views/helpers/GeolocationMapSingle.php
+++ b/views/helpers/GeolocationMapSingle.php
@@ -2,46 +2,55 @@
class Geolocation_View_Helper_GeolocationMapSingle extends Zend_View_Helper_Abstract
{
- public function geolocationMapSingle($item = null, $width = '200px', $height = '200px', $hasBalloonForMarker = false, $markerHtmlClassName = 'geolocation_balloon')
+ public function geolocationMapSingle($item = null, $width = '200px', $height = '200px')
{
$divId = "item-map-{$item->id}";
- $location = get_db()->getTable('Location')->findLocationByItem($item, true);
- // Only set the center of the map if this item actually has a location
- // associated with it
- if ($location) {
- $center['latitude'] = $location->latitude;
- $center['longitude'] = $location->longitude;
- $center['zoomLevel'] = $location->zoom_level;
- $center['show'] = true;
- if ($hasBalloonForMarker) {
- $titleLink = link_to_item(metadata($item, ['Dublin Core', 'Title'], [], $item), [], 'show', $item);
- $thumbnailLink = !(item_image('thumbnail')) ? '' : link_to_item(item_image('thumbnail', [], 0, $item), [], 'show', $item);
- $description = metadata($item, ['Dublin Core', 'Description'], ['snippet' => 150], $item);
- $center['markerHtml'] = '
'
- . '
' . $titleLink . '
'
- . '
' . $thumbnailLink . '
'
- . '
' . $description . '
';
- }
+ $locations = get_db()->getTable('Location')->findBy(['item_id' => $item->id]);
- $options = [];
- $options['basemap'] = get_option('geolocation_basemap');
- $options = $this->view->geolocationMapOptions($options);
- $center = js_escape($center);
- $varDivId = Inflector::variablize($divId);
-
- $style = "width:$width;height:$height";
- $divAttrs = [
- 'id' => $divId,
- 'class' => 'map geolocation-map',
- 'style' => $style,
- ];
+ if (empty($locations)) {
+ return '
' . __('This item has no location info associated with it.') . '
';
+ }
- $html = '
';
- $js = "var $varDivId" . "OmekaMapSingle = new OmekaMapSingle(" . js_escape($divId) . ", $center, $options); ";
- $html .= "";
- } else {
- $html = '
'.__('This item has no location info associated with it.').'
';
+ // For single-location items this sets the initial zoom correctly.
+ // For multi-location items fitLocations() overrides the center after all points are added.
+ $center = [
+ 'latitude' => $locations[0]->latitude,
+ 'longitude' => $locations[0]->longitude,
+ 'zoomLevel' => $locations[0]->zoom_level,
+ ];
+
+ $points = [];
+ foreach ($locations as $loc) {
+ $point = [
+ 'geometry_json' => $loc->geometry_json,
+ 'label' => $loc->label,
+ ];
+ if ($loc->label !== '') {
+ $point['popupHtml'] = '';
+ }
+ $points[] = $point;
}
+
+ $options = [];
+ $options['basemap'] = get_option('geolocation_basemap');
+ $options['locations'] = $points;
+ $options = $this->view->geolocationMapOptions($options);
+ $center = js_escape($center);
+ $varDivId = Inflector::variablize($divId);
+
+ $style = "width:$width;height:$height";
+ $divAttrs = [
+ 'id' => $divId,
+ 'class' => 'map geolocation-map',
+ 'style' => $style,
+ ];
+
+ $html = '
';
+ $js = "var $varDivId" . "OmekaMapSingle = new OmekaMapSingle(" . js_escape($divId) . ", $center, $options); ";
+ $html .= "";
+
return $html;
}
}
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-map.css b/views/shared/css/geolocation-map.css
new file mode 100644
index 0000000..02260ba
--- /dev/null
+++ b/views/shared/css/geolocation-map.css
@@ -0,0 +1,81 @@
+#omeka-map-form {
+ width: 100%;
+ height: 500px;
+ clear: both;
+}
+#geolocation_address {
+ width: 80%;
+ float: none;
+}
+button#geolocation_find_location_by_address {
+ float: none;
+}
+
+div#geolocation {
+ clear: both;
+}
+
+.geolocation-map {
+ z-index: 0;
+}
+
+.info-panel .map {
+ margin-top:-18px;
+ display:block;
+ margin-left:-18px;
+ margin-bottom:0;
+ border-top:3px solid #eae9db;
+ padding:0;
+}
+
+.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-popup-header {
+ margin: 0 -20px 13px;
+ padding: 8px 20px;
+ background: #e3e3e3;
+ font-weight: bold;
+}
+
+.geolocation-popup a {
+ border-bottom: none;
+}
+
+.geolocation-popup img {
+ max-width: 100%;
+}
+
+.leaflet-control-fit-all a {
+ display: flex !important;
+ align-items: center;
+ justify-content: center;
+ width: 26px;
+ height: 26px;
+ padding: 0;
+}
+
+.leaflet-control-fit-all a svg {
+ width: 14px;
+ height: 14px;
+ flex-shrink: 0;
+}
+
+img.leaflet-tile,
+img.leaflet-marker-icon,
+img.leaflet-marker-shadow {
+ padding: 0 !important;
+ border: 0 !important;
+ background-color: transparent !important;
+}
diff --git a/views/shared/css/geolocation-marker.css b/views/shared/css/geolocation-marker.css
deleted file mode 100644
index c413414..0000000
--- a/views/shared/css/geolocation-marker.css
+++ /dev/null
@@ -1,49 +0,0 @@
-#omeka-map-form {
- width: 100%;
- height: 300px;
- clear: both;
-}
-#geolocation_address {
- width: 80%;
- float: none;
-}
-button#geolocation_find_location_by_address {
- float: none;
-}
-
-div#geolocation {
- clear: both;
-}
-
-.geolocation-map {
- z-index: 0;
-}
-
-.info-panel .map {
- margin-top:-18px;
- display:block;
- margin-left:-18px;
- margin-bottom:0;
- border-top:3px solid #eae9db;
- padding:0;
-}
-
-.geolocation_balloon {
- width: 200px;
-}
-.geolocation_balloon img {
- max-width: 100%;
-}
-.geolocation_balloon_title {
- font-weight:bold;
- font-size:18px;
- margin-bottom:0px;
-}
-
-img.leaflet-tile,
-img.leaflet-marker-icon,
-img.leaflet-marker-shadow {
- padding: 0 !important;
- border: 0 !important;
- background-color: transparent !important;
-}
diff --git a/views/shared/exhibit_layouts/geolocation-map/layout.php b/views/shared/exhibit_layouts/geolocation-map/layout.php
index 1d041c7..15be75f 100644
--- a/views/shared/exhibit_layouts/geolocation-map/layout.php
+++ b/views/shared/exhibit_layouts/geolocation-map/layout.php
@@ -10,28 +10,28 @@
foreach ($attachments as $attachment):
$item = $attachment->getItem();
$file = $attachment->getFile();
- $location = $locationTable->findLocationByItem($item, true);
- if ($location):
- $titleLink = exhibit_builder_link_to_exhibit_item(null, [], $item);
+ $title = metadata($item, 'display_title', ['no_escape' => true]);
+ $titleLink = exhibit_builder_link_to_exhibit_item(null, [], $item);
- // Manually print just the caption as body when there's no file to avoid
- // double-printing the title link.
- if ($file):
- $body = $this->exhibitAttachment($attachment, [], [], true);
- else:
- $body = $this->exhibitAttachmentCaption($attachment);
- endif;
+ if ($file):
+ $body = $this->exhibitAttachment($attachment, [], [], true);
+ else:
+ $body = $this->exhibitAttachmentCaption($attachment);
+ endif;
- $html = '
'
- . '
' . $titleLink . '
'
+ $itemLocations = $locationTable->findBy(['item_id' => $item->id]);
+ foreach ($itemLocations as $location):
+ $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,
];
- endif;
+ endforeach;
endforeach;
?>
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
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/views/shared/javascripts/leaflet-draw/leaflet.draw.css b/views/shared/javascripts/leaflet-draw/leaflet.draw.css
new file mode 100644
index 0000000..a019410
--- /dev/null
+++ b/views/shared/javascripts/leaflet-draw/leaflet.draw.css
@@ -0,0 +1,10 @@
+.leaflet-draw-section{position:relative}.leaflet-draw-toolbar{margin-top:12px}.leaflet-draw-toolbar-top{margin-top:0}.leaflet-draw-toolbar-notop a:first-child{border-top-right-radius:0}.leaflet-draw-toolbar-nobottom a:last-child{border-bottom-right-radius:0}.leaflet-draw-toolbar a{background-image:url('images/spritesheet.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg');background-repeat:no-repeat;background-size:300px 30px;background-clip:padding-box}.leaflet-retina .leaflet-draw-toolbar a{background-image:url('images/spritesheet-2x.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg')}
+.leaflet-draw a{display:block;text-align:center;text-decoration:none}.leaflet-draw a .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.leaflet-draw-actions{display:none;list-style:none;margin:0;padding:0;position:absolute;left:26px;top:0;white-space:nowrap}.leaflet-touch .leaflet-draw-actions{left:32px}.leaflet-right .leaflet-draw-actions{right:26px;left:auto}.leaflet-touch .leaflet-right .leaflet-draw-actions{right:32px;left:auto}.leaflet-draw-actions li{display:inline-block}
+.leaflet-draw-actions li:first-child a{border-left:0}.leaflet-draw-actions li:last-child a{-webkit-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.leaflet-right .leaflet-draw-actions li:last-child a{-webkit-border-radius:0;border-radius:0}.leaflet-right .leaflet-draw-actions li:first-child a{-webkit-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.leaflet-draw-actions a{background-color:#919187;border-left:1px solid #AAA;color:#FFF;font:11px/19px "Helvetica Neue",Arial,Helvetica,sans-serif;line-height:28px;text-decoration:none;padding-left:10px;padding-right:10px;height:28px}
+.leaflet-touch .leaflet-draw-actions a{font-size:12px;line-height:30px;height:30px}.leaflet-draw-actions-bottom{margin-top:0}.leaflet-draw-actions-top{margin-top:1px}.leaflet-draw-actions-top a,.leaflet-draw-actions-bottom a{height:27px;line-height:27px}.leaflet-draw-actions a:hover{background-color:#a0a098}.leaflet-draw-actions-top.leaflet-draw-actions-bottom a{height:26px;line-height:26px}.leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:-2px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:0 -1px}
+.leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-31px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-29px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-62px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-60px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-92px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-90px -1px}
+.leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-122px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-120px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-273px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-271px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-152px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-150px -1px}
+.leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-182px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-180px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-212px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-210px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-242px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-240px -2px}
+.leaflet-mouse-marker{background-color:#fff;cursor:crosshair}.leaflet-draw-tooltip{background:#363636;background:rgba(0,0,0,0.5);border:1px solid transparent;-webkit-border-radius:4px;border-radius:4px;color:#fff;font:12px/18px "Helvetica Neue",Arial,Helvetica,sans-serif;margin-left:20px;margin-top:-21px;padding:4px 8px;position:absolute;visibility:hidden;white-space:nowrap;z-index:6}.leaflet-draw-tooltip:before{border-right:6px solid black;border-right-color:rgba(0,0,0,0.5);border-top:6px solid transparent;border-bottom:6px solid transparent;content:"";position:absolute;top:7px;left:-7px}
+.leaflet-error-draw-tooltip{background-color:#f2dede;border:1px solid #e6b6bd;color:#b94a48}.leaflet-error-draw-tooltip:before{border-right-color:#e6b6bd}.leaflet-draw-tooltip-single{margin-top:-12px}.leaflet-draw-tooltip-subtext{color:#f8d5e4}.leaflet-draw-guide-dash{font-size:1%;opacity:.6;position:absolute;width:5px;height:5px}.leaflet-edit-marker-selected{background-color:rgba(254,87,161,0.1);border:4px dashed rgba(254,87,161,0.6);-webkit-border-radius:4px;border-radius:4px;box-sizing:content-box}
+.leaflet-edit-move{cursor:move}.leaflet-edit-resize{cursor:pointer}.leaflet-oldie .leaflet-draw-toolbar{border:1px solid #999}
\ No newline at end of file
diff --git a/views/shared/javascripts/leaflet-draw/leaflet.draw.js b/views/shared/javascripts/leaflet-draw/leaflet.draw.js
new file mode 100644
index 0000000..71aeb33
--- /dev/null
+++ b/views/shared/javascripts/leaflet-draw/leaflet.draw.js
@@ -0,0 +1,10 @@
+/*
+ Leaflet.draw 1.0.4, a plugin that adds drawing and editing tools to Leaflet powered maps.
+ (c) 2012-2017, Jacob Toye, Jon West, Smartrak, Leaflet
+
+ https://github.com/Leaflet/Leaflet.draw
+ http://leafletjs.com
+ */
+!function(t,e,i){function o(t,e){for(;(t=t.parentElement)&&!t.classList.contains(e););return t}L.drawVersion="1.0.4",L.Draw={},L.drawLocal={draw:{toolbar:{actions:{title:"Cancel drawing",text:"Cancel"},finish:{title:"Finish drawing",text:"Finish"},undo:{title:"Delete last point drawn",text:"Delete last point"},buttons:{polyline:"Draw a polyline",polygon:"Draw a polygon",rectangle:"Draw a rectangle",circle:"Draw a circle",marker:"Draw a marker",circlemarker:"Draw a circlemarker"}},handlers:{circle:{tooltip:{start:"Click and drag to draw circle."},radius:"Radius"},circlemarker:{tooltip:{start:"Click map to place circle marker."}},marker:{tooltip:{start:"Click map to place marker."}},polygon:{tooltip:{start:"Click to start drawing shape.",cont:"Click to continue drawing shape.",end:"Click first point to close this shape."}},polyline:{error:"Error: shape edges cannot cross!",tooltip:{start:"Click to start drawing line.",cont:"Click to continue drawing line.",end:"Click last point to finish line."}},rectangle:{tooltip:{start:"Click and drag to draw rectangle."}},simpleshape:{tooltip:{end:"Release mouse to finish drawing."}}}},edit:{toolbar:{actions:{save:{title:"Save changes",text:"Save"},cancel:{title:"Cancel editing, discards all changes",text:"Cancel"},clearAll:{title:"Clear all layers",text:"Clear All"}},buttons:{edit:"Edit layers",editDisabled:"No layers to edit",remove:"Delete layers",removeDisabled:"No layers to delete"}},handlers:{edit:{tooltip:{text:"Drag handles or markers to edit features.",subtext:"Click cancel to undo changes."}},remove:{tooltip:{text:"Click on a feature to remove."}}}}},L.Draw.Event={},L.Draw.Event.CREATED="draw:created",L.Draw.Event.EDITED="draw:edited",L.Draw.Event.DELETED="draw:deleted",L.Draw.Event.DRAWSTART="draw:drawstart",L.Draw.Event.DRAWSTOP="draw:drawstop",L.Draw.Event.DRAWVERTEX="draw:drawvertex",L.Draw.Event.EDITSTART="draw:editstart",L.Draw.Event.EDITMOVE="draw:editmove",L.Draw.Event.EDITRESIZE="draw:editresize",L.Draw.Event.EDITVERTEX="draw:editvertex",L.Draw.Event.EDITSTOP="draw:editstop",L.Draw.Event.DELETESTART="draw:deletestart",L.Draw.Event.DELETESTOP="draw:deletestop",L.Draw.Event.TOOLBAROPENED="draw:toolbaropened",L.Draw.Event.TOOLBARCLOSED="draw:toolbarclosed",L.Draw.Event.MARKERCONTEXT="draw:markercontext",L.Draw=L.Draw||{},L.Draw.Feature=L.Handler.extend({initialize:function(t,e){this._map=t,this._container=t._container,this._overlayPane=t._panes.overlayPane,this._popupPane=t._panes.popupPane,e&&e.shapeOptions&&(e.shapeOptions=L.Util.extend({},this.options.shapeOptions,e.shapeOptions)),L.setOptions(this,e);var i=L.version.split(".");1===parseInt(i[0],10)&&parseInt(i[1],10)>=2?L.Draw.Feature.include(L.Evented.prototype):L.Draw.Feature.include(L.Mixin.Events)},enable:function(){this._enabled||(L.Handler.prototype.enable.call(this),this.fire("enabled",{handler:this.type}),this._map.fire(L.Draw.Event.DRAWSTART,{layerType:this.type}))},disable:function(){this._enabled&&(L.Handler.prototype.disable.call(this),this._map.fire(L.Draw.Event.DRAWSTOP,{layerType:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(L.DomUtil.disableTextSelection(),t.getContainer().focus(),this._tooltip=new L.Draw.Tooltip(this._map),L.DomEvent.on(this._container,"keyup",this._cancelDrawing,this))},removeHooks:function(){this._map&&(L.DomUtil.enableTextSelection(),this._tooltip.dispose(),this._tooltip=null,L.DomEvent.off(this._container,"keyup",this._cancelDrawing,this))},setOptions:function(t){L.setOptions(this,t)},_fireCreatedEvent:function(t){this._map.fire(L.Draw.Event.CREATED,{layer:t,layerType:this.type})},_cancelDrawing:function(t){27===t.keyCode&&(this._map.fire("draw:canceled",{layerType:this.type}),this.disable())}}),L.Draw.Polyline=L.Draw.Feature.extend({statics:{TYPE:"polyline"},Poly:L.Polyline,options:{allowIntersection:!0,repeatMode:!1,drawError:{color:"#b00b00",timeout:2500},icon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon"}),touchIcon:new L.DivIcon({iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-touch-icon"}),guidelineDistance:20,maxGuideLineLength:4e3,shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!1,clickable:!0},metric:!0,feet:!0,nautic:!1,showLength:!0,zIndexOffset:2e3,factor:1,maxPoints:0},initialize:function(t,e){L.Browser.touch&&(this.options.icon=this.options.touchIcon),this.options.drawError.message=L.drawLocal.draw.handlers.polyline.error,e&&e.drawError&&(e.drawError=L.Util.extend({},this.options.drawError,e.drawError)),this.type=L.Draw.Polyline.TYPE,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._markers=[],this._markerGroup=new L.LayerGroup,this._map.addLayer(this._markerGroup),this._poly=new L.Polyline([],this.options.shapeOptions),this._tooltip.updateContent(this._getTooltipText()),this._mouseMarker||(this._mouseMarker=L.marker(this._map.getCenter(),{icon:L.divIcon({className:"leaflet-mouse-marker",iconAnchor:[20,20],iconSize:[40,40]}),opacity:0,zIndexOffset:this.options.zIndexOffset})),this._mouseMarker.on("mouseout",this._onMouseOut,this).on("mousemove",this._onMouseMove,this).on("mousedown",this._onMouseDown,this).on("mouseup",this._onMouseUp,this).addTo(this._map),this._map.on("mouseup",this._onMouseUp,this).on("mousemove",this._onMouseMove,this).on("zoomlevelschange",this._onZoomEnd,this).on("touchstart",this._onTouch,this).on("zoomend",this._onZoomEnd,this))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._clearHideErrorTimeout(),this._cleanUpShape(),this._map.removeLayer(this._markerGroup),delete this._markerGroup,delete this._markers,this._map.removeLayer(this._poly),delete this._poly,this._mouseMarker.off("mousedown",this._onMouseDown,this).off("mouseout",this._onMouseOut,this).off("mouseup",this._onMouseUp,this).off("mousemove",this._onMouseMove,this),this._map.removeLayer(this._mouseMarker),delete this._mouseMarker,this._clearGuides(),this._map.off("mouseup",this._onMouseUp,this).off("mousemove",this._onMouseMove,this).off("zoomlevelschange",this._onZoomEnd,this).off("zoomend",this._onZoomEnd,this).off("touchstart",this._onTouch,this).off("click",this._onTouch,this)},deleteLastVertex:function(){if(!(this._markers.length<=1)){var t=this._markers.pop(),e=this._poly,i=e.getLatLngs(),o=i.splice(-1,1)[0];this._poly.setLatLngs(i),this._markerGroup.removeLayer(t),e.getLatLngs().length<2&&this._map.removeLayer(e),this._vertexChanged(o,!1)}},addVertex:function(t){if(this._markers.length>=2&&!this.options.allowIntersection&&this._poly.newLatLngIntersects(t))return void this._showErrorTooltip();this._errorShown&&this._hideErrorTooltip(),this._markers.push(this._createMarker(t)),this._poly.addLatLng(t),2===this._poly.getLatLngs().length&&this._map.addLayer(this._poly),this._vertexChanged(t,!0)},completeShape:function(){this._markers.length<=1||!this._shapeIsValid()||(this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable())},_finishShape:function(){var t=this._poly._defaultShape?this._poly._defaultShape():this._poly.getLatLngs(),e=this._poly.newLatLngIntersects(t[t.length-1]);if(!this.options.allowIntersection&&e||!this._shapeIsValid())return void this._showErrorTooltip();this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable()},_shapeIsValid:function(){return!0},_onZoomEnd:function(){null!==this._markers&&this._updateGuide()},_onMouseMove:function(t){var e=this._map.mouseEventToLayerPoint(t.originalEvent),i=this._map.layerPointToLatLng(e);this._currentLatLng=i,this._updateTooltip(i),this._updateGuide(e),this._mouseMarker.setLatLng(i),L.DomEvent.preventDefault(t.originalEvent)},_vertexChanged:function(t,e){this._map.fire(L.Draw.Event.DRAWVERTEX,{layers:this._markerGroup}),this._updateFinishHandler(),this._updateRunningMeasure(t,e),this._clearGuides(),this._updateTooltip()},_onMouseDown:function(t){if(!this._clickHandled&&!this._touchHandled&&!this._disableMarkers){this._onMouseMove(t),this._clickHandled=!0,this._disableNewMarkers();var e=t.originalEvent,i=e.clientX,o=e.clientY;this._startPoint.call(this,i,o)}},_startPoint:function(t,e){this._mouseDownOrigin=L.point(t,e)},_onMouseUp:function(t){var e=t.originalEvent,i=e.clientX,o=e.clientY;this._endPoint.call(this,i,o,t),this._clickHandled=null},_endPoint:function(e,i,o){if(this._mouseDownOrigin){var a=L.point(e,i).distanceTo(this._mouseDownOrigin),n=this._calculateFinishDistance(o.latlng);this.options.maxPoints>1&&this.options.maxPoints==this._markers.length+1?(this.addVertex(o.latlng),this._finishShape()):n<10&&L.Browser.touch?this._finishShape():Math.abs(a)<9*(t.devicePixelRatio||1)&&this.addVertex(o.latlng),this._enableNewMarkers()}this._mouseDownOrigin=null},_onTouch:function(t){var e,i,o=t.originalEvent;!o.touches||!o.touches[0]||this._clickHandled||this._touchHandled||this._disableMarkers||(e=o.touches[0].clientX,i=o.touches[0].clientY,this._disableNewMarkers(),this._touchHandled=!0,this._startPoint.call(this,e,i),this._endPoint.call(this,e,i,t),this._touchHandled=null),this._clickHandled=null},_onMouseOut:function(){this._tooltip&&this._tooltip._onMouseOut.call(this._tooltip)},_calculateFinishDistance:function(t){var e;if(this._markers.length>0){var i;if(this.type===L.Draw.Polyline.TYPE)i=this._markers[this._markers.length-1];else{if(this.type!==L.Draw.Polygon.TYPE)return 1/0;i=this._markers[0]}var o=this._map.latLngToContainerPoint(i.getLatLng()),a=new L.Marker(t,{icon:this.options.icon,zIndexOffset:2*this.options.zIndexOffset}),n=this._map.latLngToContainerPoint(a.getLatLng());e=o.distanceTo(n)}else e=1/0;return e},_updateFinishHandler:function(){var t=this._markers.length;t>1&&this._markers[t-1].on("click",this._finishShape,this),t>2&&this._markers[t-2].off("click",this._finishShape,this)},_createMarker:function(t){var e=new L.Marker(t,{icon:this.options.icon,zIndexOffset:2*this.options.zIndexOffset});return this._markerGroup.addLayer(e),e},_updateGuide:function(t){var e=this._markers?this._markers.length:0;e>0&&(t=t||this._map.latLngToLayerPoint(this._currentLatLng),this._clearGuides(),this._drawGuide(this._map.latLngToLayerPoint(this._markers[e-1].getLatLng()),t))},_updateTooltip:function(t){var e=this._getTooltipText();t&&this._tooltip.updatePosition(t),this._errorShown||this._tooltip.updateContent(e)},_drawGuide:function(t,e){var i,o,a,n=Math.floor(Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))),s=this.options.guidelineDistance,r=this.options.maxGuideLineLength,l=n>r?n-r:s;for(this._guidesContainer||(this._guidesContainer=L.DomUtil.create("div","leaflet-draw-guides",this._overlayPane));l1&&this._markers[this._markers.length-1].off("click",this._finishShape,this)},_fireCreatedEvent:function(){var t=new this.Poly(this._poly.getLatLngs(),this.options.shapeOptions);L.Draw.Feature.prototype._fireCreatedEvent.call(this,t)}}),L.Draw.Polygon=L.Draw.Polyline.extend({statics:{TYPE:"polygon"},Poly:L.Polygon,options:{showArea:!1,showLength:!1,shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0},metric:!0,feet:!0,nautic:!1,precision:{}},initialize:function(t,e){L.Draw.Polyline.prototype.initialize.call(this,t,e),this.type=L.Draw.Polygon.TYPE},_updateFinishHandler:function(){var t=this._markers.length;1===t&&this._markers[0].on("click",this._finishShape,this),t>2&&(this._markers[t-1].on("dblclick",this._finishShape,this),t>3&&this._markers[t-2].off("dblclick",this._finishShape,this))},_getTooltipText:function(){var t,e;return 0===this._markers.length?t=L.drawLocal.draw.handlers.polygon.tooltip.start:this._markers.length<3?(t=L.drawLocal.draw.handlers.polygon.tooltip.cont,e=this._getMeasurementString()):(t=L.drawLocal.draw.handlers.polygon.tooltip.end,e=this._getMeasurementString()),{text:t,subtext:e}},_getMeasurementString:function(){var t=this._area,e="";return t||this.options.showLength?(this.options.showLength&&(e=L.Draw.Polyline.prototype._getMeasurementString.call(this)),t&&(e+=" "+L.GeometryUtil.readableArea(t,this.options.metric,this.options.precision)),e):null},_shapeIsValid:function(){return this._markers.length>=3},_vertexChanged:function(t,e){var i;!this.options.allowIntersection&&this.options.showArea&&(i=this._poly.getLatLngs(),this._area=L.GeometryUtil.geodesicArea(i)),L.Draw.Polyline.prototype._vertexChanged.call(this,t,e)},_cleanUpShape:function(){var t=this._markers.length;t>0&&(this._markers[0].off("click",this._finishShape,this),t>2&&this._markers[t-1].off("dblclick",this._finishShape,this))}}),L.SimpleShape={},L.Draw.SimpleShape=L.Draw.Feature.extend({options:{repeatMode:!1},initialize:function(t,e){this._endLabelText=L.drawLocal.draw.handlers.simpleshape.tooltip.end,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._mapDraggable=this._map.dragging.enabled(),this._mapDraggable&&this._map.dragging.disable(),this._container.style.cursor="crosshair",this._tooltip.updateContent({text:this._initialLabelText}),this._map.on("mousedown",this._onMouseDown,this).on("mousemove",this._onMouseMove,this).on("touchstart",this._onMouseDown,this).on("touchmove",this._onMouseMove,this),e.addEventListener("touchstart",L.DomEvent.preventDefault,{passive:!1}))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._map&&(this._mapDraggable&&this._map.dragging.enable(),this._container.style.cursor="",this._map.off("mousedown",this._onMouseDown,this).off("mousemove",this._onMouseMove,this).off("touchstart",this._onMouseDown,this).off("touchmove",this._onMouseMove,this),L.DomEvent.off(e,"mouseup",this._onMouseUp,this),L.DomEvent.off(e,"touchend",this._onMouseUp,this),e.removeEventListener("touchstart",L.DomEvent.preventDefault),this._shape&&(this._map.removeLayer(this._shape),delete this._shape)),this._isDrawing=!1},_getTooltipText:function(){return{text:this._endLabelText}},_onMouseDown:function(t){this._isDrawing=!0,this._startLatLng=t.latlng,L.DomEvent.on(e,"mouseup",this._onMouseUp,this).on(e,"touchend",this._onMouseUp,this).preventDefault(t.originalEvent)},_onMouseMove:function(t){var e=t.latlng;this._tooltip.updatePosition(e),this._isDrawing&&(this._tooltip.updateContent(this._getTooltipText()),this._drawShape(e))},_onMouseUp:function(){this._shape&&this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable()}}),L.Draw.Rectangle=L.Draw.SimpleShape.extend({statics:{TYPE:"rectangle"},options:{shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0},showArea:!0,metric:!0},initialize:function(t,e){this.type=L.Draw.Rectangle.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.rectangle.tooltip.start,L.Draw.SimpleShape.prototype.initialize.call(this,t,e)},disable:function(){this._enabled&&(this._isCurrentlyTwoClickDrawing=!1,L.Draw.SimpleShape.prototype.disable.call(this))},_onMouseUp:function(t){if(!this._shape&&!this._isCurrentlyTwoClickDrawing)return void(this._isCurrentlyTwoClickDrawing=!0);this._isCurrentlyTwoClickDrawing&&!o(t.target,"leaflet-pane")||L.Draw.SimpleShape.prototype._onMouseUp.call(this)},_drawShape:function(t){this._shape?this._shape.setBounds(new L.LatLngBounds(this._startLatLng,t)):(this._shape=new L.Rectangle(new L.LatLngBounds(this._startLatLng,t),this.options.shapeOptions),this._map.addLayer(this._shape))},_fireCreatedEvent:function(){var t=new L.Rectangle(this._shape.getBounds(),this.options.shapeOptions);L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this,t)},_getTooltipText:function(){var t,e,i,o=L.Draw.SimpleShape.prototype._getTooltipText.call(this),a=this._shape,n=this.options.showArea;return a&&(t=this._shape._defaultShape?this._shape._defaultShape():this._shape.getLatLngs(),e=L.GeometryUtil.geodesicArea(t),i=n?L.GeometryUtil.readableArea(e,this.options.metric):""),{text:o.text,subtext:i}}}),L.Draw.Marker=L.Draw.Feature.extend({statics:{TYPE:"marker"},options:{icon:new L.Icon.Default,repeatMode:!1,zIndexOffset:2e3},initialize:function(t,e){this.type=L.Draw.Marker.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.marker.tooltip.start,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._tooltip.updateContent({text:this._initialLabelText}),this._mouseMarker||(this._mouseMarker=L.marker(this._map.getCenter(),{icon:L.divIcon({className:"leaflet-mouse-marker",iconAnchor:[20,20],iconSize:[40,40]}),opacity:0,zIndexOffset:this.options.zIndexOffset})),this._mouseMarker.on("click",this._onClick,this).addTo(this._map),this._map.on("mousemove",this._onMouseMove,this),this._map.on("click",this._onTouch,this))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._map&&(this._map.off("click",this._onClick,this).off("click",this._onTouch,this),this._marker&&(this._marker.off("click",this._onClick,this),this._map.removeLayer(this._marker),delete this._marker),this._mouseMarker.off("click",this._onClick,this),this._map.removeLayer(this._mouseMarker),delete this._mouseMarker,this._map.off("mousemove",this._onMouseMove,this))},_onMouseMove:function(t){var e=t.latlng;this._tooltip.updatePosition(e),this._mouseMarker.setLatLng(e),this._marker?(e=this._mouseMarker.getLatLng(),this._marker.setLatLng(e)):(this._marker=this._createMarker(e),this._marker.on("click",this._onClick,this),this._map.on("click",this._onClick,this).addLayer(this._marker))},_createMarker:function(t){return new L.Marker(t,{icon:this.options.icon,zIndexOffset:this.options.zIndexOffset})},_onClick:function(){this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable()},_onTouch:function(t){this._onMouseMove(t),this._onClick()},_fireCreatedEvent:function(){var t=new L.Marker.Touch(this._marker.getLatLng(),{icon:this.options.icon});L.Draw.Feature.prototype._fireCreatedEvent.call(this,t)}}),L.Draw.CircleMarker=L.Draw.Marker.extend({statics:{TYPE:"circlemarker"},options:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0,zIndexOffset:2e3},initialize:function(t,e){this.type=L.Draw.CircleMarker.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.circlemarker.tooltip.start,L.Draw.Feature.prototype.initialize.call(this,t,e)},_fireCreatedEvent:function(){var t=new L.CircleMarker(this._marker.getLatLng(),this.options);L.Draw.Feature.prototype._fireCreatedEvent.call(this,t)},_createMarker:function(t){return new L.CircleMarker(t,this.options)}}),L.Draw.Circle=L.Draw.SimpleShape.extend({statics:{TYPE:"circle"},options:{shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0},showRadius:!0,metric:!0,feet:!0,nautic:!1},initialize:function(t,e){this.type=L.Draw.Circle.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.circle.tooltip.start,L.Draw.SimpleShape.prototype.initialize.call(this,t,e)},_drawShape:function(t){if(L.GeometryUtil.isVersion07x())var e=this._startLatLng.distanceTo(t);else var e=this._map.distance(this._startLatLng,t);this._shape?this._shape.setRadius(e):(this._shape=new L.Circle(this._startLatLng,e,this.options.shapeOptions),this._map.addLayer(this._shape))},_fireCreatedEvent:function(){var t=new L.Circle(this._startLatLng,this._shape.getRadius(),this.options.shapeOptions);L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this,t)},_onMouseMove:function(t){var e,i=t.latlng,o=this.options.showRadius,a=this.options.metric;if(this._tooltip.updatePosition(i),this._isDrawing){this._drawShape(i),e=this._shape.getRadius().toFixed(1);var n="";o&&(n=L.drawLocal.draw.handlers.circle.radius+": "+L.GeometryUtil.readableDistance(e,a,this.options.feet,this.options.nautic)),this._tooltip.updateContent({text:this._endLabelText,subtext:n})}}}),L.Edit=L.Edit||{},L.Edit.Marker=L.Handler.extend({initialize:function(t,e){this._marker=t,L.setOptions(this,e)},addHooks:function(){var t=this._marker;t.dragging.enable(),t.on("dragend",this._onDragEnd,t),this._toggleMarkerHighlight()},removeHooks:function(){var t=this._marker;t.dragging.disable(),t.off("dragend",this._onDragEnd,t),this._toggleMarkerHighlight()},_onDragEnd:function(t){var e=t.target;e.edited=!0,this._map.fire(L.Draw.Event.EDITMOVE,{layer:e})},_toggleMarkerHighlight:function(){var t=this._marker._icon;t&&(t.style.display="none",L.DomUtil.hasClass(t,"leaflet-edit-marker-selected")?(L.DomUtil.removeClass(t,"leaflet-edit-marker-selected"),this._offsetMarker(t,-4)):(L.DomUtil.addClass(t,"leaflet-edit-marker-selected"),this._offsetMarker(t,4)),t.style.display="")},_offsetMarker:function(t,e){var i=parseInt(t.style.marginTop,10)-e,o=parseInt(t.style.marginLeft,10)-e;t.style.marginTop=i+"px",t.style.marginLeft=o+"px"}}),L.Marker.addInitHook(function(){L.Edit.Marker&&(this.editing=new L.Edit.Marker(this),this.options.editable&&this.editing.enable())}),L.Edit=L.Edit||{},L.Edit.Poly=L.Handler.extend({initialize:function(t){this.latlngs=[t._latlngs],t._holes&&(this.latlngs=this.latlngs.concat(t._holes)),this._poly=t,this._poly.on("revert-edited",this._updateLatLngs,this)},_defaultShape:function(){return L.Polyline._flat?L.Polyline._flat(this._poly._latlngs)?this._poly._latlngs:this._poly._latlngs[0]:this._poly._latlngs},_eachVertexHandler:function(t){for(var e=0;et&&(i._index+=e)})},_createMiddleMarker:function(t,e){var i,o,a,n=this._getMiddleLatLng(t,e),s=this._createMarker(n);s.setOpacity(.6),t._middleRight=e._middleLeft=s,o=function(){s.off("touchmove",o,this);var a=e._index;s._index=a,s.off("click",i,this).on("click",this._onMarkerClick,this),n.lat=s.getLatLng().lat,n.lng=s.getLatLng().lng,this._spliceLatLngs(a,0,n),this._markers.splice(a,0,s),s.setOpacity(1),this._updateIndexes(a,1),e._index++,this._updatePrevNext(t,s),this._updatePrevNext(s,e),this._poly.fire("editstart")},a=function(){s.off("dragstart",o,this),s.off("dragend",a,this),s.off("touchmove",o,this),this._createMiddleMarker(t,s),this._createMiddleMarker(s,e)},i=function(){o.call(this),a.call(this),this._fireEdit()},s.on("click",i,this).on("dragstart",o,this).on("dragend",a,this).on("touchmove",o,this),this._markerGroup.addLayer(s)},_updatePrevNext:function(t,e){t&&(t._next=e),e&&(e._prev=t)},_getMiddleLatLng:function(t,e){var i=this._poly._map,o=i.project(t.getLatLng()),a=i.project(e.getLatLng());return i.unproject(o._add(a)._divideBy(2))}}),L.Polyline.addInitHook(function(){this.editing||(L.Edit.Poly&&(this.editing=new L.Edit.Poly(this),this.options.editable&&this.editing.enable()),this.on("add",function(){this.editing&&this.editing.enabled()&&this.editing.addHooks()}),this.on("remove",function(){this.editing&&this.editing.enabled()&&this.editing.removeHooks()}))}),L.Edit=L.Edit||{},L.Edit.SimpleShape=L.Handler.extend({options:{moveIcon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-move"}),resizeIcon:new L.DivIcon({iconSize:new L.Point(8,8),
+className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-resize"}),touchMoveIcon:new L.DivIcon({iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-move leaflet-touch-icon"}),touchResizeIcon:new L.DivIcon({iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-resize leaflet-touch-icon"})},initialize:function(t,e){L.Browser.touch&&(this.options.moveIcon=this.options.touchMoveIcon,this.options.resizeIcon=this.options.touchResizeIcon),this._shape=t,L.Util.setOptions(this,e)},addHooks:function(){var t=this._shape;this._shape._map&&(this._map=this._shape._map,t.setStyle(t.options.editing),t._map&&(this._map=t._map,this._markerGroup||this._initMarkers(),this._map.addLayer(this._markerGroup)))},removeHooks:function(){var t=this._shape;if(t.setStyle(t.options.original),t._map){this._unbindMarker(this._moveMarker);for(var e=0,i=this._resizeMarkers.length;e "+L.drawLocal.edit.handlers.edit.tooltip.text,subtext:L.drawLocal.draw.handlers.circle.radius+": "+L.GeometryUtil.readableDistance(radius,!0,this.options.feet,this.options.nautic)}),this._shape.setRadius(radius),this._map.fire(L.Draw.Event.EDITRESIZE,{layer:this._shape})}}),L.Circle.addInitHook(function(){L.Edit.Circle&&(this.editing=new L.Edit.Circle(this),this.options.editable&&this.editing.enable())}),L.Map.mergeOptions({touchExtend:!0}),L.Map.TouchExtend=L.Handler.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane},addHooks:function(){L.DomEvent.on(this._container,"touchstart",this._onTouchStart,this),L.DomEvent.on(this._container,"touchend",this._onTouchEnd,this),L.DomEvent.on(this._container,"touchmove",this._onTouchMove,this),this._detectIE()?(L.DomEvent.on(this._container,"MSPointerDown",this._onTouchStart,this),L.DomEvent.on(this._container,"MSPointerUp",this._onTouchEnd,this),L.DomEvent.on(this._container,"MSPointerMove",this._onTouchMove,this),L.DomEvent.on(this._container,"MSPointerCancel",this._onTouchCancel,this)):(L.DomEvent.on(this._container,"touchcancel",this._onTouchCancel,this),L.DomEvent.on(this._container,"touchleave",this._onTouchLeave,this))},removeHooks:function(){L.DomEvent.off(this._container,"touchstart",this._onTouchStart,this),L.DomEvent.off(this._container,"touchend",this._onTouchEnd,this),L.DomEvent.off(this._container,"touchmove",this._onTouchMove,this),this._detectIE()?(L.DomEvent.off(this._container,"MSPointerDown",this._onTouchStart,this),L.DomEvent.off(this._container,"MSPointerUp",this._onTouchEnd,this),L.DomEvent.off(this._container,"MSPointerMove",this._onTouchMove,this),L.DomEvent.off(this._container,"MSPointerCancel",this._onTouchCancel,this)):(L.DomEvent.off(this._container,"touchcancel",this._onTouchCancel,this),L.DomEvent.off(this._container,"touchleave",this._onTouchLeave,this))},_touchEvent:function(t,e){var i={};if(void 0!==t.touches){if(!t.touches.length)return;i=t.touches[0]}else{if("touch"!==t.pointerType)return;if(i=t,!this._filterClick(t))return}var o=this._map.mouseEventToContainerPoint(i),a=this._map.mouseEventToLayerPoint(i),n=this._map.layerPointToLatLng(a);this._map.fire(e,{latlng:n,layerPoint:a,containerPoint:o,pageX:i.pageX,pageY:i.pageY,originalEvent:t})},_filterClick:function(t){var e=t.timeStamp||t.originalEvent.timeStamp,i=L.DomEvent._lastClick&&e-L.DomEvent._lastClick;return i&&i>100&&i<500||t.target._simulatedClick&&!t._simulated?(L.DomEvent.stop(t),!1):(L.DomEvent._lastClick=e,!0)},_onTouchStart:function(t){if(this._map._loaded){this._touchEvent(t,"touchstart")}},_onTouchEnd:function(t){if(this._map._loaded){this._touchEvent(t,"touchend")}},_onTouchCancel:function(t){if(this._map._loaded){var e="touchcancel";this._detectIE()&&(e="pointercancel"),this._touchEvent(t,e)}},_onTouchLeave:function(t){if(this._map._loaded){this._touchEvent(t,"touchleave")}},_onTouchMove:function(t){if(this._map._loaded){this._touchEvent(t,"touchmove")}},_detectIE:function(){var e=t.navigator.userAgent,i=e.indexOf("MSIE ");if(i>0)return parseInt(e.substring(i+5,e.indexOf(".",i)),10);if(e.indexOf("Trident/")>0){var o=e.indexOf("rv:");return parseInt(e.substring(o+3,e.indexOf(".",o)),10)}var a=e.indexOf("Edge/");return a>0&&parseInt(e.substring(a+5,e.indexOf(".",a)),10)}}),L.Map.addInitHook("addHandler","touchExtend",L.Map.TouchExtend),L.Marker.Touch=L.Marker.extend({_initInteraction:function(){return this.addInteractiveTarget?L.Marker.prototype._initInteraction.apply(this):this._initInteractionLegacy()},_initInteractionLegacy:function(){if(this.options.clickable){var t=this._icon,e=["dblclick","mousedown","mouseover","mouseout","contextmenu","touchstart","touchend","touchmove"];this._detectIE?e.concat(["MSPointerDown","MSPointerUp","MSPointerMove","MSPointerCancel"]):e.concat(["touchcancel"]),L.DomUtil.addClass(t,"leaflet-clickable"),L.DomEvent.on(t,"click",this._onMouseClick,this),L.DomEvent.on(t,"keypress",this._onKeyPress,this);for(var i=0;i0)return parseInt(e.substring(i+5,e.indexOf(".",i)),10);if(e.indexOf("Trident/")>0){var o=e.indexOf("rv:");return parseInt(e.substring(o+3,e.indexOf(".",o)),10)}var a=e.indexOf("Edge/");return a>0&&parseInt(e.substring(a+5,e.indexOf(".",a)),10)}}),L.LatLngUtil={cloneLatLngs:function(t){for(var e=[],i=0,o=t.length;i2){for(var s=0;s1&&(i=i+s+r[1])}return i},readableArea:function(e,i,o){var a,n,o=L.Util.extend({},t,o);return i?(n=["ha","m"],type=typeof i,"string"===type?n=[i]:"boolean"!==type&&(n=i),a=e>=1e6&&-1!==n.indexOf("km")?L.GeometryUtil.formattedNumber(1e-6*e,o.km)+" km²":e>=1e4&&-1!==n.indexOf("ha")?L.GeometryUtil.formattedNumber(1e-4*e,o.ha)+" ha":L.GeometryUtil.formattedNumber(e,o.m)+" m²"):(e/=.836127,a=e>=3097600?L.GeometryUtil.formattedNumber(e/3097600,o.mi)+" mi²":e>=4840?L.GeometryUtil.formattedNumber(e/4840,o.ac)+" acres":L.GeometryUtil.formattedNumber(e,o.yd)+" yd²"),a},readableDistance:function(e,i,o,a,n){var s,n=L.Util.extend({},t,n);switch(i?"string"==typeof i?i:"metric":o?"feet":a?"nauticalMile":"yards"){case"metric":s=e>1e3?L.GeometryUtil.formattedNumber(e/1e3,n.km)+" km":L.GeometryUtil.formattedNumber(e,n.m)+" m";break;case"feet":e*=3.28083,s=L.GeometryUtil.formattedNumber(e,n.ft)+" ft";break;case"nauticalMile":e*=.53996,s=L.GeometryUtil.formattedNumber(e/1e3,n.nm)+" nm";break;case"yards":default:e*=1.09361,s=e>1760?L.GeometryUtil.formattedNumber(e/1760,n.mi)+" miles":L.GeometryUtil.formattedNumber(e,n.yd)+" yd"}return s},isVersion07x:function(){var t=L.version.split(".");return 0===parseInt(t[0],10)&&7===parseInt(t[1],10)}})}(),L.Util.extend(L.LineUtil,{segmentsIntersect:function(t,e,i,o){return this._checkCounterclockwise(t,i,o)!==this._checkCounterclockwise(e,i,o)&&this._checkCounterclockwise(t,e,i)!==this._checkCounterclockwise(t,e,o)},_checkCounterclockwise:function(t,e,i){return(i.y-t.y)*(e.x-t.x)>(e.y-t.y)*(i.x-t.x)}}),L.Polyline.include({intersects:function(){var t,e,i,o=this._getProjectedPoints(),a=o?o.length:0;if(this._tooFewPointsForIntersection())return!1;for(t=a-1;t>=3;t--)if(e=o[t-1],i=o[t],this._lineSegmentsIntersectsRange(e,i,t-2))return!0;return!1},newLatLngIntersects:function(t,e){return!!this._map&&this.newPointIntersects(this._map.latLngToLayerPoint(t),e)},newPointIntersects:function(t,e){var i=this._getProjectedPoints(),o=i?i.length:0,a=i?i[o-1]:null,n=o-2;return!this._tooFewPointsForIntersection(1)&&this._lineSegmentsIntersectsRange(a,t,n,e?1:0)},_tooFewPointsForIntersection:function(t){var e=this._getProjectedPoints(),i=e?e.length:0;return i+=t||0,!e||i<=3},_lineSegmentsIntersectsRange:function(t,e,i,o){var a,n,s=this._getProjectedPoints();o=o||0;for(var r=i;r>o;r--)if(a=s[r-1],n=s[r],L.LineUtil.segmentsIntersect(t,e,a,n))return!0;return!1},_getProjectedPoints:function(){if(!this._defaultShape)return this._originalPoints;for(var t=[],e=this._defaultShape(),i=0;i=2?L.Toolbar.include(L.Evented.prototype):L.Toolbar.include(L.Mixin.Events)},enabled:function(){return null!==this._activeMode},disable:function(){this.enabled()&&this._activeMode.handler.disable()},addToolbar:function(t){var e,i=L.DomUtil.create("div","leaflet-draw-section"),o=0,a=this._toolbarClass||"",n=this.getModeHandlers(t);for(this._toolbarContainer=L.DomUtil.create("div","leaflet-draw-toolbar leaflet-bar"),this._map=t,e=0;e0&&this._singleLineLabel&&(L.DomUtil.removeClass(this._container,"leaflet-draw-tooltip-single"),this._singleLineLabel=!1):(L.DomUtil.addClass(this._container,"leaflet-draw-tooltip-single"),this._singleLineLabel=!0),this._container.innerHTML=(t.subtext.length>0?''+t.subtext+" ":"")+""+t.text+" ",t.text||t.subtext?(this._visible=!0,this._container.style.visibility="inherit"):(this._visible=!1,this._container.style.visibility="hidden"),this):this},updatePosition:function(t){var e=this._map.latLngToLayerPoint(t),i=this._container;return this._container&&(this._visible&&(i.style.visibility="inherit"),L.DomUtil.setPosition(i,e)),this},showAsError:function(){return this._container&&L.DomUtil.addClass(this._container,"leaflet-error-draw-tooltip"),this},removeError:function(){return this._container&&L.DomUtil.removeClass(this._container,"leaflet-error-draw-tooltip"),this},_onMouseOut:function(){this._container&&(this._container.style.visibility="hidden")}}),L.DrawToolbar=L.Toolbar.extend({statics:{TYPE:"draw"},options:{polyline:{},polygon:{},rectangle:{},circle:{},marker:{},circlemarker:{}},initialize:function(t){for(var e in this.options)this.options.hasOwnProperty(e)&&t[e]&&(t[e]=L.extend({},this.options[e],t[e]));this._toolbarClass="leaflet-draw-draw",L.Toolbar.prototype.initialize.call(this,t)},getModeHandlers:function(t){return[{enabled:this.options.polyline,handler:new L.Draw.Polyline(t,this.options.polyline),title:L.drawLocal.draw.toolbar.buttons.polyline},{enabled:this.options.polygon,handler:new L.Draw.Polygon(t,this.options.polygon),title:L.drawLocal.draw.toolbar.buttons.polygon},{enabled:this.options.rectangle,handler:new L.Draw.Rectangle(t,this.options.rectangle),title:L.drawLocal.draw.toolbar.buttons.rectangle},{enabled:this.options.circle,handler:new L.Draw.Circle(t,this.options.circle),title:L.drawLocal.draw.toolbar.buttons.circle},{enabled:this.options.marker,handler:new L.Draw.Marker(t,this.options.marker),title:L.drawLocal.draw.toolbar.buttons.marker},{enabled:this.options.circlemarker,handler:new L.Draw.CircleMarker(t,this.options.circlemarker),title:L.drawLocal.draw.toolbar.buttons.circlemarker}]},getActions:function(t){return[{enabled:t.completeShape,title:L.drawLocal.draw.toolbar.finish.title,text:L.drawLocal.draw.toolbar.finish.text,callback:t.completeShape,context:t},{enabled:t.deleteLastVertex,title:L.drawLocal.draw.toolbar.undo.title,text:L.drawLocal.draw.toolbar.undo.text,callback:t.deleteLastVertex,context:t},{title:L.drawLocal.draw.toolbar.actions.title,text:L.drawLocal.draw.toolbar.actions.text,callback:this.disable,context:this}]},setOptions:function(t){L.setOptions(this,t);for(var e in this._modes)this._modes.hasOwnProperty(e)&&t.hasOwnProperty(e)&&this._modes[e].handler.setOptions(t[e])}}),L.EditToolbar=L.Toolbar.extend({statics:{TYPE:"edit"},options:{edit:{selectedPathOptions:{dashArray:"10, 10",fill:!0,fillColor:"#fe57a1",fillOpacity:.1,maintainColor:!1}},remove:{},poly:null,featureGroup:null},initialize:function(t){t.edit&&(void 0===t.edit.selectedPathOptions&&(t.edit.selectedPathOptions=this.options.edit.selectedPathOptions),t.edit.selectedPathOptions=L.extend({},this.options.edit.selectedPathOptions,t.edit.selectedPathOptions)),t.remove&&(t.remove=L.extend({},this.options.remove,t.remove)),t.poly&&(t.poly=L.extend({},this.options.poly,t.poly)),this._toolbarClass="leaflet-draw-edit",L.Toolbar.prototype.initialize.call(this,t),this._selectedFeatureCount=0},getModeHandlers:function(t){var e=this.options.featureGroup;return[{enabled:this.options.edit,handler:new L.EditToolbar.Edit(t,{featureGroup:e,selectedPathOptions:this.options.edit.selectedPathOptions,poly:this.options.poly}),title:L.drawLocal.edit.toolbar.buttons.edit},{enabled:this.options.remove,handler:new L.EditToolbar.Delete(t,{featureGroup:e}),title:L.drawLocal.edit.toolbar.buttons.remove}]},getActions:function(t){var e=[{title:L.drawLocal.edit.toolbar.actions.save.title,text:L.drawLocal.edit.toolbar.actions.save.text,callback:this._save,context:this},{title:L.drawLocal.edit.toolbar.actions.cancel.title,text:L.drawLocal.edit.toolbar.actions.cancel.text,callback:this.disable,context:this}];return t.removeAllLayers&&e.push({title:L.drawLocal.edit.toolbar.actions.clearAll.title,text:L.drawLocal.edit.toolbar.actions.clearAll.text,callback:this._clearAllLayers,context:this}),e},addToolbar:function(t){var e=L.Toolbar.prototype.addToolbar.call(this,t);return this._checkDisabled(),this.options.featureGroup.on("layeradd layerremove",this._checkDisabled,this),e},removeToolbar:function(){this.options.featureGroup.off("layeradd layerremove",this._checkDisabled,this),L.Toolbar.prototype.removeToolbar.call(this)},disable:function(){this.enabled()&&(this._activeMode.handler.revertLayers(),L.Toolbar.prototype.disable.call(this))},_save:function(){this._activeMode.handler.save(),this._activeMode&&this._activeMode.handler.disable()},_clearAllLayers:function(){this._activeMode.handler.removeAllLayers(),this._activeMode&&this._activeMode.handler.disable()},_checkDisabled:function(){var t,e=this.options.featureGroup,i=0!==e.getLayers().length;this.options.edit&&(t=this._modes[L.EditToolbar.Edit.TYPE].button,i?L.DomUtil.removeClass(t,"leaflet-disabled"):L.DomUtil.addClass(t,"leaflet-disabled"),t.setAttribute("title",i?L.drawLocal.edit.toolbar.buttons.edit:L.drawLocal.edit.toolbar.buttons.editDisabled)),this.options.remove&&(t=this._modes[L.EditToolbar.Delete.TYPE].button,i?L.DomUtil.removeClass(t,"leaflet-disabled"):L.DomUtil.addClass(t,"leaflet-disabled"),t.setAttribute("title",i?L.drawLocal.edit.toolbar.buttons.remove:L.drawLocal.edit.toolbar.buttons.removeDisabled))}}),L.EditToolbar.Edit=L.Handler.extend({statics:{TYPE:"edit"},initialize:function(t,e){if(L.Handler.prototype.initialize.call(this,t),L.setOptions(this,e),this._featureGroup=e.featureGroup,!(this._featureGroup instanceof L.FeatureGroup))throw new Error("options.featureGroup must be a L.FeatureGroup");this._uneditedLayerProps={},this.type=L.EditToolbar.Edit.TYPE;var i=L.version.split(".");1===parseInt(i[0],10)&&parseInt(i[1],10)>=2?L.EditToolbar.Edit.include(L.Evented.prototype):L.EditToolbar.Edit.include(L.Mixin.Events)},enable:function(){!this._enabled&&this._hasAvailableLayers()&&(this.fire("enabled",{handler:this.type}),this._map.fire(L.Draw.Event.EDITSTART,{handler:this.type}),L.Handler.prototype.enable.call(this),this._featureGroup.on("layeradd",this._enableLayerEdit,this).on("layerremove",this._disableLayerEdit,this))},disable:function(){this._enabled&&(this._featureGroup.off("layeradd",this._enableLayerEdit,this).off("layerremove",this._disableLayerEdit,this),L.Handler.prototype.disable.call(this),this._map.fire(L.Draw.Event.EDITSTOP,{handler:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(t.getContainer().focus(),this._featureGroup.eachLayer(this._enableLayerEdit,this),this._tooltip=new L.Draw.Tooltip(this._map),this._tooltip.updateContent({text:L.drawLocal.edit.handlers.edit.tooltip.text,subtext:L.drawLocal.edit.handlers.edit.tooltip.subtext}),t._editTooltip=this._tooltip,this._updateTooltip(),this._map.on("mousemove",this._onMouseMove,this).on("touchmove",this._onMouseMove,this).on("MSPointerMove",this._onMouseMove,this).on(L.Draw.Event.EDITVERTEX,this._updateTooltip,this))},removeHooks:function(){this._map&&(this._featureGroup.eachLayer(this._disableLayerEdit,this),this._uneditedLayerProps={},this._tooltip.dispose(),this._tooltip=null,this._map.off("mousemove",this._onMouseMove,this).off("touchmove",this._onMouseMove,this).off("MSPointerMove",this._onMouseMove,this).off(L.Draw.Event.EDITVERTEX,this._updateTooltip,this))},revertLayers:function(){this._featureGroup.eachLayer(function(t){this._revertLayer(t)},this)},save:function(){var t=new L.LayerGroup;this._featureGroup.eachLayer(function(e){e.edited&&(t.addLayer(e),e.edited=!1)}),this._map.fire(L.Draw.Event.EDITED,{layers:t})},_backupLayer:function(t){var e=L.Util.stamp(t);this._uneditedLayerProps[e]||(t instanceof L.Polyline||t instanceof L.Polygon||t instanceof L.Rectangle?this._uneditedLayerProps[e]={latlngs:L.LatLngUtil.cloneLatLngs(t.getLatLngs())}:t instanceof L.Circle?this._uneditedLayerProps[e]={latlng:L.LatLngUtil.cloneLatLng(t.getLatLng()),radius:t.getRadius()}:(t instanceof L.Marker||t instanceof L.CircleMarker)&&(this._uneditedLayerProps[e]={latlng:L.LatLngUtil.cloneLatLng(t.getLatLng())}))},_getTooltipText:function(){return{text:L.drawLocal.edit.handlers.edit.tooltip.text,subtext:L.drawLocal.edit.handlers.edit.tooltip.subtext}},_updateTooltip:function(){this._tooltip.updateContent(this._getTooltipText())},_revertLayer:function(t){var e=L.Util.stamp(t);t.edited=!1,this._uneditedLayerProps.hasOwnProperty(e)&&(t instanceof L.Polyline||t instanceof L.Polygon||t instanceof L.Rectangle?t.setLatLngs(this._uneditedLayerProps[e].latlngs):t instanceof L.Circle?(t.setLatLng(this._uneditedLayerProps[e].latlng),t.setRadius(this._uneditedLayerProps[e].radius)):(t instanceof L.Marker||t instanceof L.CircleMarker)&&t.setLatLng(this._uneditedLayerProps[e].latlng),t.fire("revert-edited",{layer:t}))},_enableLayerEdit:function(t){var e,i,o=t.layer||t.target||t;this._backupLayer(o),this.options.poly&&(i=L.Util.extend({},this.options.poly),o.options.poly=i),this.options.selectedPathOptions&&(e=L.Util.extend({},this.options.selectedPathOptions),e.maintainColor&&(e.color=o.options.color,e.fillColor=o.options.fillColor),o.options.original=L.extend({},o.options),o.options.editing=e),o instanceof L.Marker?(o.editing&&o.editing.enable(),o.dragging.enable(),o.on("dragend",this._onMarkerDragEnd).on("touchmove",this._onTouchMove,this).on("MSPointerMove",this._onTouchMove,this).on("touchend",this._onMarkerDragEnd,this).on("MSPointerUp",this._onMarkerDragEnd,this)):o.editing.enable()},_disableLayerEdit:function(t){var e=t.layer||t.target||t;e.edited=!1,e.editing&&e.editing.disable(),delete e.options.editing,delete e.options.original,
+this._selectedPathOptions&&(e instanceof L.Marker?this._toggleMarkerHighlight(e):(e.setStyle(e.options.previousOptions),delete e.options.previousOptions)),e instanceof L.Marker?(e.dragging.disable(),e.off("dragend",this._onMarkerDragEnd,this).off("touchmove",this._onTouchMove,this).off("MSPointerMove",this._onTouchMove,this).off("touchend",this._onMarkerDragEnd,this).off("MSPointerUp",this._onMarkerDragEnd,this)):e.editing.disable()},_onMouseMove:function(t){this._tooltip.updatePosition(t.latlng)},_onMarkerDragEnd:function(t){var e=t.target;e.edited=!0,this._map.fire(L.Draw.Event.EDITMOVE,{layer:e})},_onTouchMove:function(t){var e=t.originalEvent.changedTouches[0],i=this._map.mouseEventToLayerPoint(e),o=this._map.layerPointToLatLng(i);t.target.setLatLng(o)},_hasAvailableLayers:function(){return 0!==this._featureGroup.getLayers().length}}),L.EditToolbar.Delete=L.Handler.extend({statics:{TYPE:"remove"},initialize:function(t,e){if(L.Handler.prototype.initialize.call(this,t),L.Util.setOptions(this,e),this._deletableLayers=this.options.featureGroup,!(this._deletableLayers instanceof L.FeatureGroup))throw new Error("options.featureGroup must be a L.FeatureGroup");this.type=L.EditToolbar.Delete.TYPE;var i=L.version.split(".");1===parseInt(i[0],10)&&parseInt(i[1],10)>=2?L.EditToolbar.Delete.include(L.Evented.prototype):L.EditToolbar.Delete.include(L.Mixin.Events)},enable:function(){!this._enabled&&this._hasAvailableLayers()&&(this.fire("enabled",{handler:this.type}),this._map.fire(L.Draw.Event.DELETESTART,{handler:this.type}),L.Handler.prototype.enable.call(this),this._deletableLayers.on("layeradd",this._enableLayerDelete,this).on("layerremove",this._disableLayerDelete,this))},disable:function(){this._enabled&&(this._deletableLayers.off("layeradd",this._enableLayerDelete,this).off("layerremove",this._disableLayerDelete,this),L.Handler.prototype.disable.call(this),this._map.fire(L.Draw.Event.DELETESTOP,{handler:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(t.getContainer().focus(),this._deletableLayers.eachLayer(this._enableLayerDelete,this),this._deletedLayers=new L.LayerGroup,this._tooltip=new L.Draw.Tooltip(this._map),this._tooltip.updateContent({text:L.drawLocal.edit.handlers.remove.tooltip.text}),this._map.on("mousemove",this._onMouseMove,this))},removeHooks:function(){this._map&&(this._deletableLayers.eachLayer(this._disableLayerDelete,this),this._deletedLayers=null,this._tooltip.dispose(),this._tooltip=null,this._map.off("mousemove",this._onMouseMove,this))},revertLayers:function(){this._deletedLayers.eachLayer(function(t){this._deletableLayers.addLayer(t),t.fire("revert-deleted",{layer:t})},this)},save:function(){this._map.fire(L.Draw.Event.DELETED,{layers:this._deletedLayers})},removeAllLayers:function(){this._deletableLayers.eachLayer(function(t){this._removeLayer({layer:t})},this),this.save()},_enableLayerDelete:function(t){(t.layer||t.target||t).on("click",this._removeLayer,this)},_disableLayerDelete:function(t){var e=t.layer||t.target||t;e.off("click",this._removeLayer,this),this._deletedLayers.removeLayer(e)},_removeLayer:function(t){var e=t.layer||t.target||t;this._deletableLayers.removeLayer(e),this._deletedLayers.addLayer(e),e.fire("deleted")},_onMouseMove:function(t){this._tooltip.updatePosition(t.latlng)},_hasAvailableLayers:function(){return 0!==this._deletableLayers.getLayers().length}})}(window,document);
\ No newline at end of file
diff --git a/views/shared/javascripts/map.js b/views/shared/javascripts/map.js
index 0db5898..f0f7658 100644
--- a/views/shared/javascripts/map.js
+++ b/views/shared/javascripts/map.js
@@ -5,73 +5,97 @@ function OmekaMap(mapDivId, center, options) {
}
OmekaMap.prototype = {
-
+
map: null,
mapDivId: null,
- markers: [],
- options: {},
center: null,
- markerBounds: null,
+ options: {},
+ locations: [],
+ locationBounds: null,
clusterGroup: null,
-
- addMarker: function (latLng, options, bindHtml)
- {
+ deflateGroup: null,
+
+ addMarker: function (latLng, options, bindHtml) {
var map = this.map;
var marker = L.marker(latLng, options);
- var srAlertsDiv = jQuery('#geolocation-sr-alerts');
- var srAlertStringArray = [marker.options.title, srAlertsDiv.data('latString'), latLng[0], srAlertsDiv.data('longString'), latLng[1]];
- var srOpenedAlertStringArray = srAlertStringArray.concat([srAlertsDiv.data('openedString')]);
- var srClosedAlertStringArray = srAlertStringArray.concat([srAlertsDiv.data('closedString')]);
if (this.clusterGroup) {
this.clusterGroup.addLayer(marker);
} else {
marker.addTo(map);
}
-
+
if (bindHtml) {
marker.bindPopup(bindHtml, {autoPanPadding: [50, 50]});
- // Fit images on the map on first load
- marker.once('popupopen', function (event) {
- var popup = event.popup;
- var imgs = popup.getElement().getElementsByTagName('img');
- for (var i = 0; i < imgs.length; i++) {
- imgs[i].addEventListener('load', function imgLoadListener(event) {
- event.target.removeEventListener('load', imgLoadListener);
- // Marker autopan is disabled during panning, so defer
- if (map._panAnim && map._panAnim._inProgress) {
- map.once('moveend', function () {
- popup.update();
- });
- } else {
- popup.update();
- }
- });
- }
- });
+ }
- marker.addEventListener('popupopen', function (event) {
- srAlertsDiv.text(srOpenedAlertStringArray.join(' '));
- });
+ this.locations.push(marker);
+ this.locationBounds.extend(latLng);
+ return marker;
+ },
- marker.addEventListener('popupclose', function (event) {
- srAlertsDiv.text(srClosedAlertStringArray.join(' '));
- });
+ fitLocations: function () {
+ if (!this.locationBounds.isValid()) {
+ return;
}
-
- this.markers.push(marker);
- this.markerBounds.extend(latLng);
- return marker;
+ 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.locations.push(layer);
+ this.locationBounds.extend(layer.getBounds());
+ return layer;
},
- fitMarkers: function () {
- if (this.markers.length == 1) {
- this.map.panTo(this.markers[0].getLatLng());
- } else if (this.markers.length > 0) {
- this.map.fitBounds(this.markerBounds, {padding: [25, 25]});
+ 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 +105,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.locations = [];
L.tileLayer.provider(this.options.basemap, this.options.basemapOptions).addTo(this.map);
@@ -100,34 +125,60 @@ OmekaMap.prototype = {
this.clusterGroup = L.markerClusterGroup({
showCoverageOnHover: false
});
- this.map.addLayer(this.clusterGroup);
}
+ // When clustering is enabled, markerLayer routes collapsed shapes into
+ // the cluster group so they cluster alongside point markers. When it is
+ // disabled clusterGroup is null and Leaflet.Deflate falls back to its
+ // own FeatureGroup.
+ 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);
- // Show the center marker if we have that enabled.
- if (this.center.show) {
- this.addMarker([this.center.latitude, this.center.longitude],
- {title: "(" + this.center.latitude + ',' + this.center.longitude + ")"},
- this.center.markerHtml);
- }
+ new OmekaFitControl({ position: 'topleft', omekaMap: this }).addTo(this.map);
}
};
+var OmekaFitControl = L.Control.extend({
+ onAdd: function () {
+ var omekaMap = this.options.omekaMap;
+ 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.fitAllLocations;
+ link.setAttribute('role', 'button');
+ 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.fitLocations();
+ });
+ this._link = link;
+ return container;
+ },
+ onRemove: function () {
+ L.DomEvent.off(this._link, 'click');
+ }
+});
+
function OmekaMapBrowse(mapDivId, center, options) {
var omekaMap = new OmekaMap(mapDivId, center, options);
jQuery.extend(true, this, omekaMap);
this.initMap();
-
- //XML loads asynchronously, so need to call for further config only after it has executed
- this.loadKmlIntoMap(this.options.uri, this.options.params);
+ this.loadLocationsIntoMap(this.options.uri, this.options.params);
}
OmekaMapBrowse.prototype = {
-
+
afterLoadItems: function () {
- if (this.options.fitMarkers) {
- this.fitMarkers();
+ if (this.options.fitLocations) {
+ this.fitLocations();
}
if (!this.options.list) {
@@ -138,118 +189,93 @@ OmekaMapBrowse.prototype = {
if (!listDiv.length) {
alert('Error: You have no map links div!');
} else {
- //Create HTML links for each of the markers
this.buildListLinks(listDiv);
}
},
-
- /* Need to parse KML manually b/c Google Maps API cannot access the KML
- behind the admin interface */
- loadKmlIntoMap: function (kmlUrl, params) {
+
+ loadLocationsIntoMap: function (url, params) {
var that = this;
jQuery.ajax({
type: 'GET',
- dataType: 'xml',
- url: kmlUrl,
+ dataType: 'json',
+ url: url,
data: params,
- success: function(data) {
- var xml = jQuery(data);
-
- /* KML can be parsed as:
- kml - root element
- Placemark
- namewithlink
- description
- Point - longitude,latitude
- */
- var placeMarks = xml.find('Placemark');
-
- // If we have some placemarks, load them
- if (placeMarks.length) {
- // Retrieve the balloon styling from the KML file
- that.browseBalloon = that.getBalloonStyling(xml);
-
- // Build the markers from the placemarks
- jQuery.each(placeMarks, function (index, placeMark) {
- placeMark = jQuery(placeMark);
- that.buildMarkerFromPlacemark(placeMark);
+ success: function(locations) {
+ if (locations.length) {
+ jQuery.each(locations, function (index, locationData) {
+ that.buildLayerFromLocation(locationData);
});
-
- // We have successfully loaded some map points, so continue setting up the map object
return that.afterLoadItems();
- } else {
- // @todo Elaborate with an error message
- return false;
- }
+ }
}
});
},
-
- getBalloonStyling: function (xml) {
- return xml.find('BalloonStyle text').text();
+
+ buildLayerFromLocation: function (locationData) {
+ var geometry = JSON.parse(locationData.geometry_json);
+ var layer = this.addLayerFromGeometry(geometry, {title: locationData.title, alt: locationData.title}, this.buildLocationContent(locationData));
+ // _geolocationTitle is the label this location shows in the sidebar list.
+ layer._geolocationTitle = locationData.title || '';
},
-
- // Build a marker given the KML XML Placemark data
- // I wish we could use the KML file directly, but it's behind the admin interface so no go
- buildMarkerFromPlacemark: function (placeMark) {
- // Get the info for each location on the map
- var title = placeMark.find('name').text();
- var titleWithLink = placeMark.find('namewithlink').text();
- var body = placeMark.find('description').text();
- var snippet = placeMark.find('Snippet').text();
-
- // Extract the lat/long from the KML-formatted data
- var coordinates = placeMark.find('Point coordinates').text().split(',');
- var longitude = coordinates[0];
- var latitude = coordinates[1];
-
- // Use the KML formatting (do some string sub magic)
- var balloon = this.browseBalloon;
- balloon = balloon.replace('$[namewithlink]', titleWithLink).replace('$[description]', body).replace('$[Snippet]', snippet);
-
- // Build a marker, add HTML for it
- this.addMarker([latitude, longitude], {title: title, alt: title}, balloon);
+
+ buildLocationContent: function (locationData) {
+ var popup = jQuery('