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 @@
- +
-

+

formCheckbox('cluster', true, ['checked' => (bool) get_option('geolocation_cluster')]); ?>
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'] = '
' + . '
' . html_escape($loc->label) . '
' + . '
'; + } + $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 = '
' + . '
' . $headerText . '
' . $body + . '
' . $titleLink . '
' . '
'; $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('
'); + var headerText = locationData.label || locationData.title; + popup.append(jQuery('
').text(headerText)); + if (locationData.thumbnailUrl) { + var img = jQuery('').attr({src: locationData.thumbnailUrl, alt: ''}); + var thumbLink = jQuery('').addClass('view-item').attr('href', locationData.itemUrl).append(img); + popup.append(jQuery('
').append(thumbLink)); + } + var titleLink = jQuery('').addClass('view-item').attr('href', locationData.itemUrl).text(locationData.title); + popup.append(jQuery('
').append(titleLink)); + if (locationData.snippet) { + popup.append(jQuery('

').text(locationData.snippet)); + } + return popup[0]; }, - + buildListLinks: function (container) { var that = this; var list = jQuery('

    '); list.appendTo(container); - // Loop through all the markers - jQuery.each(this.markers, function (index, marker) { - var listElement = jQuery('
  • '); - - // Make an
    tag, give it a class for styling - var link = jQuery(''); - link.addClass('item-link'); - - // Links open up the markers on the map, clicking them doesn't actually go anywhere - link.attr('href', 'javascript:void(0);'); - link.attr('role', 'button'); - - // Each
  • starts with the title of the item - link.text(marker.options.title); - - // Clicking the link should take us to the map - link.bind('click', {}, function (event) { - link.toggleClass('current'); - - if (that.clusterGroup) { - that.clusterGroup.zoomToShowLayer(marker, function () { - marker.fire('click'); - }); + // this.locations holds points and shapes in load order, so the list + // interleaves them as they were added rather than grouping by type. + jQuery.each(this.locations, function (index, layer) { + that._buildListItem(list, layer._geolocationTitle, function () { + if (layer instanceof L.Marker) { + if (that.clusterGroup) { + that.clusterGroup.zoomToShowLayer(layer, function () { + layer.fire('click'); + }); + } else { + that.map.once('moveend', function () { + layer.fire('click'); + }); + that.map.flyTo(layer.getLatLng()); + } } else { that.map.once('moveend', function () { - marker.fire('click'); + layer.openPopup(); }); - that.map.flyTo(marker.getLatLng()); + that.map.fitBounds(layer.getBounds()); } }); + }); + }, - link.appendTo(listElement); - listElement.appendTo(list); + _buildListItem: function (list, title, onClick) { + var link = jQuery('') + .addClass('item-link') + .attr('href', 'javascript:void(0);') + .attr('role', 'button') + .text(title); + link.bind('click', {}, function () { + link.toggleClass('current'); + onClick(); }); + jQuery('
  • ').append(link).appendTo(list); } }; @@ -257,6 +283,13 @@ function OmekaMapSingle(mapDivId, center, options) { var omekaMap = new OmekaMap(mapDivId, center, options); jQuery.extend(true, this, omekaMap); this.initMap(); + if (options.locations && options.locations.length) { + for (var i = 0; i < options.locations.length; i++) { + var pt = options.locations[i]; + this.addLayerFromGeometry(JSON.parse(pt.geometry_json), {title: pt.label, alt: pt.label}, pt.popupHtml); + } + this.fitLocations(); + } } function OmekaMapForm(mapDivId, center, options) { @@ -264,102 +297,127 @@ function OmekaMapForm(mapDivId, center, options) { var omekaMap = new OmekaMap(mapDivId, center, options); jQuery.extend(true, this, omekaMap); this.initMap(); - - this.formDiv = jQuery('#' + this.options.form.id); - - // Make the map clickable to add a location point. - this.map.on('click', function (event) { - // If we are clicking a new spot on the map - var marker = that.setMarker(event.latlng.wrap()); - if (marker) { - jQuery('#geolocation_address').val(''); + + this.jsonInput = document.getElementById('geolocation-locations-json'); + + // Leaflet.draw's edit and delete toolbars require a FeatureGroup to operate on. + this.drawnItems = new L.FeatureGroup(); + this.map.addLayer(this.drawnItems); + + L.drawLocal.edit.toolbar.buttons.edit = options.strings.editLocations; + L.drawLocal.edit.toolbar.buttons.editDisabled = options.strings.noLocationsToEdit; + L.drawLocal.edit.toolbar.buttons.remove = options.strings.deleteLocations; + L.drawLocal.edit.toolbar.buttons.removeDisabled = options.strings.noLocationsToDelete; + + var drawControl = new L.Control.Draw({ + position: 'topleft', + draw: { + marker: true, + polyline: true, + polygon: true, + rectangle: true, + circle: false, + circlemarker: false, + }, + edit: { + featureGroup: this.drawnItems, + edit: true, + remove: true, + }, + }); + this.map.addControl(drawControl); + + this.map.on(L.Draw.Event.CREATED, function (event) { + var isMarker = event.layerType === 'marker'; + var layer = that.addLocation({ + geometry_json: JSON.stringify(event.layer.toGeoJSON().geometry), + zoom_level: isMarker ? that.map.getZoom() : 0 + }); + if (isMarker) { + layer.openPopup(); } }); - - // Make the map update on zoom changes. + + this.map.on(L.Draw.Event.EDITED, function (event) { + event.layers.eachLayer(function (layer) { + layer._locationData.geometry_json = JSON.stringify(layer.toGeoJSON().geometry); + if (layer instanceof L.Marker) { + var latlng = layer.getLatLng(); + layer._locationData.latitude = latlng.lat; + layer._locationData.longitude = latlng.lng; + } + }); + }); + this.map.on('zoomend', function () { - that.updateZoomForm(); + var zoom = that.map.getZoom(); + that.drawnItems.eachLayer(function (layer) { + layer._locationData.zoom_level = zoom; + }); }); - // Add the existing map point. - if (this.options.point) { - var point = L.latLng(this.options.point.latitude, this.options.point.longitude); - this.setMarker(point); - this.map.setView(point, this.options.point.zoomLevel); - } + jQuery(this.jsonInput).closest('form').on('submit', function () { + var data = []; + that.drawnItems.eachLayer(function (layer) { + data.push(layer._locationData); + }); + jQuery(that.jsonInput).val(JSON.stringify(data)); + }); } OmekaMapForm.prototype = { - /* Set the marker to the point. */ - setMarker: function (point) { - var that = this; - if (this.options.confirmLocationChange - && this.markers.length > 0 - && !confirm('Are you sure you want to change the location of the item?') - ) { - return false; - } + // Adds an editable layer (point marker or shape) from a location object and + // records its _locationData for form submission. locationData needs only a + // geometry_json string; id/zoom_level/address/label default sensibly. + addLocation: function (locationData) { + var geometry = JSON.parse(locationData.geometry_json); + var layer = L.GeoJSON.geometryToLayer(geometry); + this.drawnItems.addLayer(layer); + this.locations.push(layer); - // Get rid of existing markers. - this.clearForm(); - - // Add the marker - var marker = this.addMarker(point); - - // Pan the map to the marker - this.map.panTo(point); - - // Make the marker clear the form if clicked. - marker.on('click', function (event) { - if (!that.options.confirmLocationChange || confirm('Are you sure you want to remove the location of the item?')) { - that.clearForm(); - } - }); - - this.updateForm(point); - return marker; - }, - - /* Update the latitude, longitude, and zoom of the form. */ - updateForm: function (point) { - var latElement = document.getElementsByName('geolocation[latitude]')[0]; - var lngElement = document.getElementsByName('geolocation[longitude]')[0]; - var zoomElement = document.getElementsByName('geolocation[zoom_level]')[0]; - - // If we passed a point, then set the form to that. If there is no point, clear the form - if (point) { - latElement.value = point.lat; - lngElement.value = point.lng; - zoomElement.value = this.map.getZoom(); + // A point is represented by its own position; a shape by its bounding + // box, with the box center standing in as its point (matching how + // Location::beforeSave derives lat/lng server-side). `extent` is the + // shape we grow the map's overall bounds by. + var center, extent; + if (geometry.type === 'Point') { + extent = center = layer.getLatLng(); } else { - latElement.value = ''; - lngElement.value = ''; - zoomElement.value = this.map.getZoom(); - } + extent = layer.getBounds(); + center = extent.getCenter(); + } + this.locationBounds.extend(extent); + + layer._locationData = { + id: locationData.id || null, + latitude: center.lat, + longitude: center.lng, + zoom_level: locationData.zoom_level || 0, + address: locationData.address || '', + label: locationData.label || '', + geometry_json: locationData.geometry_json + }; + + this._bindLabelPopup(layer, layer._locationData.label); + + return layer; }, - - /* Update the zoom input of the form to be the current zoom on the map. */ - updateZoomForm: function () { - var zoomElement = document.getElementsByName('geolocation[zoom_level]')[0]; - zoomElement.value = this.map.getZoom(); + + _bindLabelPopup: function (layer, initialLabel) { + var labelInput = jQuery('').val(initialLabel); + var popupContent = jQuery('
    ') + .append(jQuery('').text(this.options.strings.label + ': ').append(labelInput)); + layer.bindPopup(popupContent[0], {autoPanPadding: [50, 50]}); + labelInput.on('input', function () { + layer._locationData.label = jQuery(this).val(); + }); }, - - /* Clear the form of all markers. */ - clearForm: function () { - // Remove the markers from the map - for (var i = 0; i < this.markers.length; i++) { - this.markers[i].remove(); - } - - // Clear the markers array - this.markers = []; - - // Update the form - this.updateForm(); + + getLocationCount: function () { + return this.drawnItems.getLayers().length; }, - - /* Resize the map and center it on the first marker. */ + resize: function () { this.map.invalidateSize(); } diff --git a/views/shared/map/browse-json.php b/views/shared/map/browse-json.php new file mode 100644 index 0000000..394fd9c --- /dev/null +++ b/views/shared/map/browse-json.php @@ -0,0 +1,26 @@ +id] ?? []; + $title = metadata($item, 'display_title', ['no_escape' => true]); + $thumbnailUrl = metadata($item, 'has thumbnail') ? record_image_url($item, 'thumbnail') : ''; + $snippet = (string) metadata($item, ['Dublin Core', 'Description'], ['snippet' => 150]); + $itemUrl = record_url($item, 'show', true); + foreach ($itemLocations as $location): + $output[] = [ + 'id' => (int) $location->id, + 'latitude' => (float) $location->latitude, + 'longitude' => (float) $location->longitude, + 'zoom_level' => (int) $location->zoom_level, + 'address' => $location->address, + 'label' => $location->label, + 'geometry_json' => $location->geometry_json, + 'title' => $title, + 'thumbnailUrl' => $thumbnailUrl, + 'snippet' => $snippet, + 'itemId' => (int) $item->id, + 'itemUrl' => $itemUrl, + ]; + endforeach; +endforeach; +echo json_encode($output); diff --git a/views/shared/map/browse.kml.php b/views/shared/map/browse.kml.php deleted file mode 100644 index 35c42c4..0000000 --- a/views/shared/map/browse.kml.php +++ /dev/null @@ -1,39 +0,0 @@ -'; ?> - - - Omeka Items KML - - - id]; - ?> - - true])); ?> - 'view-item'])); ?> - 150])); ?> - '']), ['class' => 'view-item'])); - } - ?> - - , - - -
    - -
    - -
    -
    diff --git a/views/shared/map/input-partial.php b/views/shared/map/input-partial.php index 2f4b4a2..6dca998 100644 --- a/views/shared/map/input-partial.php +++ b/views/shared/map/input-partial.php @@ -3,22 +3,20 @@ $options = $this->geolocationMapOptions($options); ?> - - - - + +
    - +
    -
    +