-
-
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/controllers/IndexController.php b/controllers/IndexController.php
old mode 100644
new mode 100755
index 6443be3..5672dbd
--- a/controllers/IndexController.php
+++ b/controllers/IndexController.php
@@ -1,214 +1,198 @@
'Place',
- self::ITEM_TYPE_ID_EVENT => 'Event',
- self::ITEM_TYPE_ID_DOCUMENT => 'Document',
- self::ITEM_TYPE_ID_STILL_IMAGE => 'Image', // Still Image
- self::ITEM_TYPE_ID_MOVING_IMAGE => 'Video', // Moving Image
- self::ITEM_TYPE_ID_SOUND => 'Audio', // Sound
- );
-
- /**
- * @var array Data used when adding the historic map layer.
+ * Return an associative array of public tours
+ *
*/
- private $_historicMapData = array(
- 'Pre-1800s' => array(
- 'url' => '/mallhistory/plugins/MallMap/maps/1791/{z}/{x}/{y}.jpg',
- 'title' => 'Map by Faehtz, E.F.M. (1791)',
- ),
- '1800-1829' => array(
- 'url' => '/mallhistory/plugins/MallMap/maps/1828/{z}/{x}/{y}.jpg',
- 'title' => 'Map by Elliot, William (1828)',
- ),
- '1830-1859' => array(
- 'url' => '/mallhistory/plugins/MallMap/maps/1858/{z}/{x}/{y}.jpg',
- 'title' => 'Map by Boschke, A. (1858)',
- ),
- '1860-1889' => array(
- 'url' => '/mallhistory/plugins/MallMap/maps/1887/{z}/{x}/{y}.jpg',
- 'title' => 'Map by Silversparre, Axel (1887)',
- ),
- '1890-1919' => array(
- 'url' => '/mallhistory/plugins/MallMap/maps/1917/{z}/{x}/{y}.jpg',
- 'title' => 'Map by U.S. Public Buildings Commission (1917)',
- ),
- '1920-1949' => array(
- 'url' => '/mallhistory/plugins/MallMap/maps/1942/{z}/{x}/{y}.jpg',
- 'title' => 'Map by General Drafting Company (1942)',
- ),
- '1950-1979' => array(
- 'url' => '/mallhistory/plugins/MallMap/maps/1978/{z}/{x}/{y}.jpg',
- 'title' => 'Map by Alexandria Drafting Company (1978)',
- ),
- '1980-1999' => array(
- 'url' => '/mallhistory/plugins/MallMap/maps/1996/{z}/{x}/{y}.jpg',
- 'title' => 'Map by Joseph Passonneau and Partners (1996)',
- ),
- //'2000-present' => array('url' => null, 'title' => null),
- );
-
+ public function publicTours()
+ {
+ // Get the database.
+ $db = get_db();
+ // Get the Tour table.
+ $tour_table = $db->getTable('Tour');
+ // Build the select query.
+ $select = $tour_table->getSelect();
+ // Fetch some items with our select.
+ $results = $tour_table->fetchObjects($select);
+ // Build an array with
+ $_tourTypes = array('id' => array(), 'color' => array());
+ foreach ($results as $tour) {
+ if ($tour['public'] == 1 || current_user()->role == "super") {
+ $_tourTypes['id'][$tour['id']] = $tour['title'];
+ $_tourTypes['color'][$tour['id']] = $tour['color'];
+ $_tourTypes['description'][$tour['id']] = $tour['description'];
+ $_tourTypes['credits'][$tour['id']] = $tour['credits'];
+ }
+ }
+
+ return $_tourTypes;
+ }
+
+ // Save the route for a tour
+ public function saveRouteAction() {
+ $tourId = $this->getRequest()->getPost('tour_id');
+ $route = $this->getRequest()->getPost('route');
+ $db = get_db();
+ $tourTable = $db->getTable('Tour');
+ $tour = $tourTable->find($tourId);
+ if ($tour) {
+ $tour->route = $route;
+ $tour->save();
+ $this->_helper->json(array('success' => true, 'message' => 'Route saved successfully.'));
+ } else {
+ $this->_helper->json(array('success' => false, 'message' => 'Tour not found.'));
+ }
+ }
+
/**
* Display the map.
*/
public function indexAction()
{
- $simpleVocabTerm = $this->_helper->db->getTable('SimpleVocabTerm');
-
- $mapCoverages = $simpleVocabTerm->findByElementId(self::ELEMENT_ID_MAP_COVERAGE);
- $placeTypes = $simpleVocabTerm->findByElementId(self::ELEMENT_ID_PLACE_TYPE);
- $eventTypes = $simpleVocabTerm->findByElementId(self::ELEMENT_ID_EVENT_TYPE);
-
- $this->view->item_types = $this->_itemTypes;
- $this->view->map_coverages = explode("\n", $mapCoverages->terms);
- $this->view->place_types = explode("\n", $placeTypes->terms);
- $this->view->event_types = explode("\n", $eventTypes->terms);
-
+ $_tourTypes = $this->publicTours();
+ $this->view->tour_types = $_tourTypes;
+
// Set the JS and CSS files.
$this->view->headScript()
->appendFile('//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js')
->appendFile('//ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js')
->appendFile(src('jquery.cookie', 'javascripts', 'js'))
- ->appendFile('//cdn.leafletjs.com/leaflet-0.7/leaflet.js')
- ->appendFile(src('mall-map', 'javascripts', 'js'))
- ->appendFile(src('modernizr.custom.63332', 'javascripts', 'js'));
+ ->appendFile(src('/leaflet/leaflet', 'javascripts', 'js'))
+ ->appendFile(src('modernizr.custom.63332', 'javascripts', 'js'))
+ ->appendFile(src('Polyline.encoded', 'javascripts', 'js'))
+ ->appendFile('//cdn.jsdelivr.net/npm/@allmaps/leaflet/dist/bundled/allmaps-leaflet-1.9.umd.js')
+ ->appendFile(src('walking-tour', 'javascripts', 'js'));
$this->view->headLink()
->appendStylesheet('//code.jquery.com/ui/1.10.2/themes/smoothness/jquery-ui.css', 'all')
- ->appendStylesheet('//cdn.leafletjs.com/leaflet-0.7/leaflet.css', 'all')
- ->appendStylesheet('//cdn.leafletjs.com/leaflet-0.7/leaflet.ie.css', 'all', 'lte IE 8')
- ->appendStylesheet(src('MarkerCluster', 'css', 'css'))
- ->appendStylesheet(src('MarkerCluster.Default', 'css', 'css'))
- ->appendStylesheet(src('MarkerCluster.Default.ie', 'css', 'css'), 'all', 'lte IE 8')
- ->appendStylesheet(src('mall-map', 'css', 'css'));
+ // ->appendStylesheet('//cdn.leafletjs.com/leaflet-0.7/leaflet.css', 'all')
+ // ->appendStylesheet('//cdn.leafletjs.com/leaflet-0.7/leaflet.ie.css', 'all', 'lte IE 8')
+ ->appendStylesheet(src('walking-tour', 'css', 'css'));
+ // ->appendStylesheet(src('/../../../themes/mall-theme', 'css', 'css'));
}
-
- /**
- * Filter items that have been geolocated by the Geolocation plugin.
- *
- * Since this is mobile-first, optimized SQL queries are preferable to using
- * the Omeka API.
- */
- public function filterAction()
+
+ public function mapConfigAction()
{
// Process only AJAX requests.
if (!$this->_request->isXmlHttpRequest()) {
throw new Omeka_Controller_Exception_403;
}
-
- $db = $this->_helper->db->getDb();
- $joins = array("$db->Item AS items ON items.id = locations.item_id");
- $wheres = array("items.public = 1");
-
- // Filter item type.
- if ($this->_request->getParam('itemType')) {
- $wheres[] = $db->quoteInto("items.item_type_id = ?", $this->_request->getParam('itemType'), Zend_Db::INT_TYPE);
- }
- // Filter map coverage.
- if ($this->_request->getParam('mapCoverage')) {
- $alias = "map_coverage";
- $joins[] = "$db->ElementText AS $alias ON $alias.record_id = items.id AND $alias.record_type = 'Item' "
- . $db->quoteInto("AND $alias.element_id = ?", self::ELEMENT_ID_MAP_COVERAGE);
- $wheres[] = $db->quoteInto("$alias.text = ?", $this->_request->getParam('mapCoverage'));
- }
- // Filter place types (inclusive).
- if ($this->_request->getParam('placeTypes')) {
- $alias = "place_types";
- $joins[] = "$db->ElementText AS $alias ON $alias.record_id = items.id AND $alias.record_type = 'Item' "
- . $db->quoteInto("AND $alias.element_id = ?", self::ELEMENT_ID_PLACE_TYPE);
- $placeTypes = array();
- foreach ($this->_request->getParam('placeTypes') as $text) {
- $placeTypes[] = $db->quoteInto("$alias.text = ?", $text);
- }
- $wheres[] = implode(" OR ", $placeTypes);
- // Filter event types (inclusive).
- } else if ($this->_request->getParam('eventTypes')) {
- $alias = "event_types";
- $joins[] = "$db->ElementText AS $alias ON $alias.record_id = items.id AND $alias.record_type = 'Item' "
- . $db->quoteInto("AND $alias.element_id = ?", self::ELEMENT_ID_EVENT_TYPE);
- $eventTypes = array();
- foreach ($this->_request->getParam('eventTypes') as $text) {
- $eventTypes[] = $db->quoteInto("$alias.text = ?", $text);
- }
- $wheres[] = implode(" OR ", $eventTypes);
- }
-
- // Build the SQL.
- $sql = "SELECT items.id, locations.latitude, locations.longitude\nFROM $db->Location AS locations";
- foreach ($joins as $join) {
- $sql .= "\nJOIN $join";
- }
- foreach ($wheres as $key => $where) {
- $sql .= (0 == $key) ? "\nWHERE" : "\nAND";
- $sql .= " ($where)";
- }
- $sql .= "\nGROUP BY items.id";
-
- // Build geoJSON: http://www.geojson.org/geojson-spec.html
- $data = array('type' => 'FeatureCollection', 'features' => array());
- foreach ($db->query($sql)->fetchAll() as $row) {
- $data['features'][] = array(
- 'type' => 'Feature',
- 'geometry' => array(
- 'type' => 'Point',
- 'coordinates' => array($row['longitude'], $row['latitude']),
- ),
- 'properties' => array(
- 'id' => $row['id'],
- ),
- );
- }
- $this->_helper->json($data);
+
+ $returnArray = array();
+ $returnArray['walking_tour_center'] = get_option('walking_tour_center');
+ $returnArray['walking_tour_default_zoom'] = get_option('walking_tour_default_zoom');
+ $returnArray['walking_tour_max_zoom'] = get_option('walking_tour_max_zoom');
+ $returnArray['walking_tour_min_zoom'] = get_option('walking_tour_min_zoom');
+ $returnArray['walking_tour_max_bounds'] = get_option('walking_tour_max_bounds');
+ $returnArray['walking_tour_exhibit_button'] = get_option('walking_tour_exhibit_button');
+ $returnArray['walking_tour_detail_button'] = get_option('walking_tour_detail_button');
+ $returnArray['walking_tour_auto_fit'] = get_option('walking_tour_auto_fit');
+
+ $this->_helper->json($returnArray);
}
-
- /**
- * Get data about the selected historical map.
+
+ /*
+ * Beginning to separate tours into separate features
*/
- public function historicMapDataAction()
+ public function queryAction()
{
// Process only AJAX requests.
if (!$this->_request->isXmlHttpRequest()) {
throw new Omeka_Controller_Exception_403;
}
- if (!isset($this->_historicMapData[$this->_request->getParam('text')])) {
- throw new Omeka_Controller_Exception_404;
+
+ $db = $this->_helper->db->getDb();
+ $joins = array("$db->Item AS items ON items.id = locations.item_id");
+ $wheres = array("items.public = 1");
+ $prefix = $db->prefix;
+
+ // Filter public tours' items
+ $request_tour_id = $this->publicTours();
+ $colorArray = array();
+
+ $tourItemTable = $db->getTable('TourItem');
+ $tourItemsIDs = array();
+ $returnArray = array();
+ foreach ($request_tour_id['id'] as $tour_id => $tour_title) {
+ if ($tour_id != 0) {
+ $tourItemsDat = $tourItemTable->fetchObjects("SELECT item_id FROM " . $prefix . "tour_items
+ WHERE tour_id = $tour_id");
+ } else {
+ $tourItemsDat = $tourItemTable->fetchObjects("SELECT item_id FROM " . $prefix . "tour_items");
+ }
+ $tourItemsIDs[$tour_id] = array();
+ foreach ($tourItemsDat as $dat) {
+ array_push($tourItemsIDs[$tour_id], (int) $dat["item_id"]);
+ }
}
- $data = $this->_historicMapData[$this->_request->getParam('text')];
- $this->_helper->json($data);
+
+ foreach ($tourItemsIDs as $tour_id => $item_array) {
+
+ $tourItemsID = implode(", ", $item_array);
+ $wheres = array("items.public = 1");
+ $wheres[] = $db->quoteInto("items.id IN ($tourItemsID)", Zend_Db::INT_TYPE);
+
+ $sql = "SELECT items.id, locations.latitude, locations.longitude\nFROM $db->Location AS locations";
+ foreach ($joins as $join) {
+ $sql .= "\nJOIN $join";
+ }
+ foreach ($wheres as $key => $where) {
+ $sql .= (0 == $key) ? "\nWHERE" : "\nAND";
+ $sql .= " ($where)";
+ }
+ $sql .= "\nGROUP BY items.id";
+
+ $dbItems = $db->query($sql)->fetchAll();
+ $orderedItems = array();
+
+ // orders items to match the order of the tour
+ for ($i = 0; $i < count($item_array); $i++) {
+ for ($j = 0; $j < count($dbItems); $j++) {
+ if ($item_array[$i] == $dbItems[$j]['id']) {
+ array_push($orderedItems, $dbItems[$j]);
+ }
+ }
+ }
+ // Build geoJSON: http://www.geojson.org/geojson-spec.html
+ $returnArray[$tour_id]["Data"] = array('type' => 'FeatureCollection', 'features' => array());
+ foreach ($orderedItems as $row) {
+ $returnArray[$tour_id]["Data"]['features'][] = array(
+ 'type' => 'Feature',
+ 'geometry' => array(
+ 'type' => 'Point',
+ 'coordinates' => array($row['longitude'], $row['latitude']),
+ ),
+ 'properties' => array(
+ 'id' => $row['id'],
+ "marker-color" => $request_tour_id['color'][$tour_id]
+ ),
+ );
+ }
+ $returnArray[$tour_id]["Color"] = $request_tour_id['color'][$tour_id];
+ $returnArray[$tour_id]["Tour Name"] = $request_tour_id['id'][$tour_id];
+ $returnArray[$tour_id]["Description"] = $request_tour_id['description'][$tour_id];
+ $returnArray[$tour_id]["Credits"] = $request_tour_id['credits'][$tour_id];
+
+ $tourTable = $db->getTable('Tour');
+ $tour = $tourTable->find($tour_id);
+ $returnArray[$tour_id]["Route"] = $tour ? $tour->route : null;
+ }
+ $this->_helper->json($returnArray);
+
}
-
+
/**
* Get data about the selected item.
*/
@@ -218,20 +202,48 @@ public function getItemAction()
if (!$this->_request->isXmlHttpRequest()) {
throw new Omeka_Controller_Exception_403;
}
- $item = get_record_by_id('item', $this->_request->getParam('id'));
+ $item_id = $this->_request->getParam('id');
+ $tour_id = $this->_request->getParam('tour');
+
+ $db = $this->_helper->db->getDb();
+ $tourItemTable = $db->getTable('TourItem');
+ $prefix = $db->prefix;
+
+
+ $tourItem = $tourItemTable->fetchObjects("SELECT * FROM " . $prefix . "tour_items
+ WHERE tour_id = $tour_id AND item_id = $item_id");
+
+ $exhibit_id = $tourItem[0]["exhibit_id"];
+
+ $item = get_record_by_id('item', $item_id);
$data = array(
- 'id' => $item->id,
- 'title' => metadata($item, array('Dublin Core', 'Title')),
- 'description' => metadata($item, array('Dublin Core', 'Description')),
- 'date' => metadata($item, array('Dublin Core', 'Date'), array('all' => true)),
- 'thumbnail' => item_image('square_thumbnail', array(), 0, $item),
- 'fullsize' => item_image('fullsize', array('style' => 'max-width: 100%; height: auto;'), 0, $item),
- 'url' => url(array('module' => 'default',
- 'controller' => 'items',
- 'action' => 'show',
- 'id' => $item['id']),
- 'id'),
+ 'id' => $item->id,
+ 'title' => metadata($item, array('Dublin Core', 'Title')),
+ 'description' => metadata($item, array('Dublin Core', 'Description'), array('no-escape' => true)),
+ // 'abstract' => metadata($item, array('Dublin Core', 'Abstract'), array('no-escape' => true)),
+ 'date' => metadata($item, array('Dublin Core', 'Date'), array('all' => true)),
+ 'thumbnail' => item_image('square_thumbnail', array(), 0, $item),
+ 'fullsize' => item_image('fullsize', array('style' => 'max-width: 100%; height: auto;'), 0, $item),
+ 'url' => url(
+ array(
+ 'module' => 'default',
+ 'controller' => 'items',
+ 'action' => 'show',
+ 'id' => $item['id']
+ ),
+ 'id'
+ ),
+ "exhibitUrl" => ""
);
+ if (plugin_is_active('DublinCoreExtended')) {
+ $data['abstract'] = metadata($item, array('Dublin Core', 'Abstract'), array('no-escape' => true));
+ }
+ if (plugin_is_active('ExhibitBuilder')) {
+ $exhibit = get_records('Exhibit', array('id' => $exhibit_id));
+ if ($exhibit && count($exhibit) == 1) {
+ $data["exhibitUrl"] = exhibit_builder_exhibit_uri($exhibit[0]);
+ }
+ }
$this->_helper->json($data);
}
}
diff --git a/controllers/ToursController.php b/controllers/ToursController.php
new file mode 100644
index 0000000..696c735
--- /dev/null
+++ b/controllers/ToursController.php
@@ -0,0 +1,12 @@
+_helper->db->setDefaultModelName( 'Tour' );
+ }
+
+}
diff --git a/helpers/TourFunctions.php b/helpers/TourFunctions.php
new file mode 100644
index 0000000..d51ef56
--- /dev/null
+++ b/helpers/TourFunctions.php
@@ -0,0 +1,143 @@
+prefix;
+ $itemTable = $db->getTable( 'Item' );
+ $locationTable = $db->getTable( 'Location' );
+ $locationItemsDat = $locationTable->fetchObjects( "SELECT item_id FROM ".$prefix."locations");
+ if ($locationItemsDat) {
+ $locationItemsIDs = array();
+ foreach ($locationItemsDat as $dat){
+ $locationItemsIDs[] = (int) $dat["item_id"];
+ }
+ $locationItemsIDs = implode(", ", $locationItemsIDs);
+ $items = $itemTable->fetchObjects( "SELECT * FROM ".$prefix."items WHERE id IN ($locationItemsIDs) ORDER BY modified DESC" );
+ foreach($items as $key => $arr) {
+ $items[$key]['label'] = metadata( $arr, array( 'Dublin Core', 'Title' ) );
+ }
+ return json_encode($items);
+ }
+}
+
+function availableExhibit() {
+ $db = get_db();
+ $prefix=$db->prefix;
+ $exhibitTable = $db->getTable( 'Exhibit' );
+ if ($exhibitTable){
+ $items = $exhibitTable->fetchObjects( "SELECT * FROM ".$prefix."exhibits ORDER BY id DESC" );
+ return json_encode($items);
+ }
+}
+
+function has_tours()
+{
+ return( total_tours() > 0 );
+}
+
+function has_tours_for_loop()
+{
+ $view = get_view();
+ return $view->tours && count( $view->tours );
+}
+
+
+function tour( $fieldName, $options=array(), $tour=null )
+{
+ if( ! $tour ) {
+ $tour = get_current_tour();
+ }
+
+ switch( strtolower( $fieldName ) ) {
+ case 'id':
+ $text = $tour->id;
+ break;
+ case 'title':
+ $text = $tour->title;
+ break;
+ case 'description':
+ $text = $tour->description;
+ break;
+ case 'credits':
+ $text = $tour->credits;
+ break;
+ case 'postscript_text':
+ $text = $tour->postscript_text;
+ break;
+ default:
+ throw new Exception( "\"$fieldName\" does not exist for tours!" );
+ break;
+ }
+
+ if( isset( $options['snippet'] ) ) {
+ $text = snippet( $text, 0, (int)$options['snippet'] );
+ }
+
+ if( !is_array( $text ) ) {
+ $text = html_escape( $text );
+ } else {
+ $text = array_map( 'html_escape', $text );
+
+ if( isset( $options['delimiter'] ) ) {
+ $text = join( (string) $options['delimiter'], (array) $text );
+ }
+ }
+
+ return $text;
+}
+
+function get_current_tour()
+{
+ return get_view()->tour;
+}
+
+function link_to_tour(
+ $text=null, $props=array(), $action='show', $tourObj = null )
+{
+ # Use the current tour object if none given
+ if( ! $tourObj ) {
+ $tourObj = get_current_tour();
+ }
+
+ # Create default text, if it was not passed in.
+ if( empty( $text ) ) {
+ $tourName = tour('title', array(), $tourObj);
+ $text = (! empty( $tourName )) ? $tourName : '[Untitled]';
+ }
+
+ return link_to($tourObj, $action, $text, $props);
+}
+
+
+function total_tours()
+{
+ $view = get_view();
+ return count( $view->tours );
+}
+
+function nls2p($str) {
+ $str = str_replace('
', '', '
'
+ . preg_replace('#([
+]\s*?[
+]){2,}#', '
', $str)
+ . '
');
+ return $str;
+}
+
+/*
+** Get an ID of an item in a tour
+** $tour sets the tour object
+** $i is used to choose the position in the item array
+** USAGE: tour_item_id($this->tour,0)
+*/
+function tour_item_id($tour,$i){
+ $tourItems =array();
+ foreach( $tour->Items as $items ){
+ array_push($tourItems,$items->id);
+ }
+ return isset($tourItems[$i]) ? $tourItems[$i] : null;
+}
diff --git a/images/AddATour.png b/images/AddATour.png
new file mode 100644
index 0000000..b4bac52
Binary files /dev/null and b/images/AddATour.png differ
diff --git a/images/ChangeColor.png b/images/ChangeColor.png
new file mode 100644
index 0000000..5417036
Binary files /dev/null and b/images/ChangeColor.png differ
diff --git a/images/EditTour.png b/images/EditTour.png
new file mode 100644
index 0000000..cf541b7
Binary files /dev/null and b/images/EditTour.png differ
diff --git a/images/LinkExhibit.png b/images/LinkExhibit.png
new file mode 100644
index 0000000..0e1fd2b
Binary files /dev/null and b/images/LinkExhibit.png differ
diff --git a/images/RemoveItem.png b/images/RemoveItem.png
new file mode 100644
index 0000000..cd40f17
Binary files /dev/null and b/images/RemoveItem.png differ
diff --git a/images/SampleTourStop.png b/images/SampleTourStop.png
new file mode 100644
index 0000000..2038a7c
Binary files /dev/null and b/images/SampleTourStop.png differ
diff --git a/images/ViewExhibit.png b/images/ViewExhibit.png
new file mode 100644
index 0000000..2cd157d
Binary files /dev/null and b/images/ViewExhibit.png differ
diff --git a/images/WakingTourTab.png b/images/WakingTourTab.png
new file mode 100644
index 0000000..7754fa5
Binary files /dev/null and b/images/WakingTourTab.png differ
diff --git a/images/walkingTourImg.png b/images/walkingTourImg.png
new file mode 100644
index 0000000..b0b60c7
Binary files /dev/null and b/images/walkingTourImg.png differ
diff --git a/models/Tour.php b/models/Tour.php
new file mode 100644
index 0000000..cac1f09
--- /dev/null
+++ b/models/Tour.php
@@ -0,0 +1,131 @@
+ 'getItems','Image' => 'getImage' );
+
+ public function _initializeMixins()
+ {
+ $this->_mixins[] = new Mixin_Search($this);
+ }
+
+ public function getItems()
+ {
+ return $this->getTable()->findItemsByTourId( $this->id );
+ }
+
+
+ public function removeAllItems( ) {
+ $db = get_db();
+ $tiTable = $db->getTable( 'TourItem' );
+ $select = $tiTable->getSelect();
+ $select->where( 'tour_id = ?', array( $this->id ) );
+
+ # Get the tour item
+ $tourItems = $tiTable->fetchObjects( $select );
+
+ # Iterate through all the tour items
+ # and remove them
+ for($i = 0; $i < count($tourItems); $i++) {
+ $tourItems[$i]->delete();
+ }
+ }
+
+ public function addItem( $item_id, $exhibit_id = 0 , $ordinal = null )
+ {
+ if( !is_numeric( $item_id ) ) {
+ $item_id = $item_id->id;
+ }
+
+ # Get the next ordinal
+ $db = get_db();
+ $tiTable = $db->getTable( 'TourItem' );
+ $select = $tiTable->getSelectForCount();
+ $select->where( 'tour_id = ?', array( $this->id ) );
+ if($ordinal === null) {
+ $ordinal = $tiTable->fetchOne( $select );
+ }
+
+ # Create, assign, and save the new tour item connection
+ $tourItem = new TourItem;
+ $tourItem->tour_id = $this->id;
+ $tourItem->item_id = $item_id;
+ $tourItem->ordinal = $ordinal;
+ if (plugin_is_active('ExhibitBuilder')){
+ $tourItem->exhibit_id = $exhibit_id;
+ }
+ $tourItem->save();
+ }
+
+
+ protected function _validate()
+ {
+ if( empty( $this->title ) ) {
+ $this->addError( 'title', 'Tour must be given a title.' );
+ }
+
+ if( strlen( $this->title ) > 255 ) {
+ $this->addError( 'title', 'Title for a tour must be 255 characters or fewer.' );
+ }
+ if (!$this->fieldIsUnique('title')) {
+ $this->addError('title', 'The Title is already in use by another tour. Please choose another.');
+ }
+
+ }
+
+ protected function beforeDelete(){
+ $this->removeAllItems();
+ }
+
+ protected function afterSave($args)
+ {
+ $post=$args['post'];
+ if($post && isset($post['tour_item_ids']) && !$args['insert']){
+ $this->removeAllItems();
+
+
+ // Get item IDs from $_POST and save to tour items table
+ $tour_item_ids=trim( $post['tour_item_ids'] );
+ $item_ids=explode( ',', $tour_item_ids );
+
+ $tour_item_exhibit_ids=trim( $post['tour_item_exhibit_ids'] );
+ $item_exhibit_ids=explode( ',', $tour_item_exhibit_ids );
+ $i=0;
+ $index = 0;
+
+
+ foreach($item_ids as $item_id){
+ $item_id=intval($item_id);
+ $exhibit_id = intval($item_exhibit_ids[$index]);
+ if($item_id){
+ $this->addItem( $item_id, $exhibit_id, $i);
+ $i++;
+ }
+ $index++;
+ }
+ }
+
+ // Add tour to search index
+ if (!$this->public) {
+ $this->setSearchTextPrivate();
+ }
+ $this->setSearchTextTitle($this->title);
+ $this->addSearchText($this->title);
+ $this->addSearchText($this->description);
+ }
+}
diff --git a/models/TourItem.php b/models/TourItem.php
new file mode 100644
index 0000000..0553116
--- /dev/null
+++ b/models/TourItem.php
@@ -0,0 +1,55 @@
+ 'getTour',
+ 'Item' => 'getItem',
+ );
+
+ protected function getItem()
+ {
+ return $this->getTable( 'Item' )->find( $this->item_id );
+ }
+
+ protected function getTour()
+ {
+ return $this->getTable( 'Tour' )->find( $this->tour_id );
+ }
+
+ protected function _validate()
+ {
+ if( empty( $this->item_id ) ) {
+ $this->addError( 'item_id', 'Tour item requires an item ID#' );
+ }
+
+ if( ! is_numeric( $this->item_id ) ) {
+ $this->addError( 'item_id', 'Item ID must be numeric' );
+ }
+
+ if( empty( $this->tour_id ) ) {
+ $this->addError( 'tour_id', 'Tour item requires a tour ID#' );
+ }
+
+ if( ! is_numeric( $this->tour_id ) ) {
+ $this->addError( 'tour_id', 'Tour ID must be numeric' );
+ }
+
+ if( ! is_numeric( $this->ordinal ) ) {
+ $this->addError( 'ordinal', 'Order must be numeric' );
+ }
+
+ if( ! is_numeric( $this->exhibit_id ) ) {
+ $this->addError( 'exhibit_id', 'Exhibit must be numeric id' );
+ }
+ }
+}
diff --git a/models/TourTable.php b/models/TourTable.php
new file mode 100644
index 0000000..1f5de47
--- /dev/null
+++ b/models/TourTable.php
@@ -0,0 +1,68 @@
+prefix;
+ $itemTable = $this->getTable( 'Item' );
+ $select = $itemTable->getSelect();
+ $iAlias = $itemTable->getTableAlias();
+ $select->joinInner( array( 'ti' => $db->TourItem ),
+ "ti.item_id = $iAlias.id", array() );
+ $select->where( 'ti.tour_id = ?', array( $tour_id ) );
+ $select->order( 'ti.ordinal ASC' );
+
+ $items = $itemTable->fetchObjects( "SELECT i.*, ti.ordinal, ti.exhibit_id
+ FROM ".$prefix."items i LEFT JOIN ".$prefix."tour_items ti
+ ON i.id = ti.item_id
+ WHERE ti.tour_id = ?
+ ORDER BY ti.ordinal ASC",
+ array( $tour_id ) );
+
+ //$items = $itemTable->fetchObjects( $select );
+ return $items;
+ }
+
+ public function findImageByTourId( $tour_id ) {
+ $db = get_db();
+ $prefix=$db->prefix;
+ $itemTable = $this->getTable( 'File' );
+ $select = $itemTable->getSelect();
+ $iAlias = $itemTable->getTableAlias();
+ $select->joinInner( array( 'ti' => $db->TourItem ),
+ "ti.item_id = $iAlias.id", array() );
+ $select->where( 'ti.tour_id = ?', array( $tour_id ) );
+ $select->order( 'ti.ordinal ASC' );
+
+ $items = $itemTable->fetchObjects( "SELECT f.*, ti.ordinal
+ FROM ".$prefix."files f LEFT JOIN ".$prefix."tour_items ti
+ ON i.id = ti.item_id
+ WHERE ti.tour_id = ?
+ ORDER BY ti.ordinal ASC",
+ array( $tour_id ) );
+
+ //$items = $itemTable->fetchObjects( $select );
+ return $items;
+ }
+
+ public function getSelect()
+ {
+ $select = parent::getSelect()->order('tours.id');
+
+ $permissions = new Omeka_Db_Select_PublicPermissions( 'WalkingTourBuilder_Tours' );
+ $permissions->apply( $select, 'tours', null );
+ $acl = Zend_Registry::get('bootstrap')->getResource('Acl');
+/*
+ if( $acl && ! is_allowed( 'WalkingTourBuilder_Tours', 'show-unpublished' ) )
+ {
+ // Determine public level TODO: May be outdated
+ $select->where( $this->getTableAlias() . '.public = 1' );
+ }
+*/
+
+ return $select;
+ }
+
+}
diff --git a/plugin.ini b/plugin.ini
old mode 100644
new mode 100755
index 2b2c1a8..6de57f6
--- a/plugin.ini
+++ b/plugin.ini
@@ -1,12 +1,12 @@
[info]
-name="Mall Map"
-author="Roy Rosenzweig Center for History and New Media"
-description=""
+name="Walking Tour"
+author="Tingjun Tu, Alejandro González, Alec Wang, and Austin Mason"
+description="Adds the ability to create and display walking tours on a map"
license="GPLv3"
link=""
-support_link=""
-version="1.0-dev"
-omeka_minimum_version="2.0"
-omeka_target_version="2.0"
-tags=""
-required_plugins="Geolocation,SimpleVocab"
+support_link="https://github.com/DigitalCarleton/WalkingTour"
+version="1.0.1"
+omeka_minimum_version="3.0"
+omeka_target_version="3.1.2"
+tags="map, tour"
+required_plugins="Geolocation"
diff --git a/routes.ini b/routes.ini
new file mode 100644
index 0000000..0fbe13a
--- /dev/null
+++ b/routes.ini
@@ -0,0 +1,22 @@
+[routes]
+tours.route = "tours/:action"
+tours.defaults.module = walking-tour
+tours.defaults.controller = tours
+tours.defaults.action = "browse"
+
+tourAction.route = "tours/:action/:id"
+tourAction.defaults.module = walking-tour
+tourAction.defaults.controller = tours
+tourAction.defaults.action = "show"
+tourAction.reqs.id = "\d+"
+
+tourItemAction.route = "tours/edit/:id/:action/:item"
+tourItemAction.defaults.module = walking-tour
+tourItemAction.defaults.controller = tours
+tourItemAction.reqs.id = "\d+"
+tourItemAction.reqs.item = "\d+"
+
+oldTour.route = "tour-builder/tours/:action/:id"
+oldTour.defaults.module = walking-tour
+oldTour.defaults.controller = tours
+oldTour.defaults.action = "browse"
diff --git a/views/admin/css/tour-1.7.css b/views/admin/css/tour-1.7.css
new file mode 100644
index 0000000..a357198
--- /dev/null
+++ b/views/admin/css/tour-1.7.css
@@ -0,0 +1,111 @@
+#admin-tour-image img{max-width: 100%; }
+#tour.edit #admin-tour-image img{margin: 1em 0;}
+#tour.show #admin-tour-image img{margin: 0 0;}
+#tour.edit input[type="text"]{margin-bottom:0; width:100%;}
+#tour.edit input#image{background:#eaeaea;padding:.5em 0 .5em .5em;width: 100%;border: 1px solid #D8D8D8;}
+#tour.edit p.explanation{padding:.25em 0;}
+#tour.edit .file-helper{display: inline-block;font-style: italic;padding: .5em 0 0;}
+
+#tour.browse .fa{
+ font-family:"FontAwesome";
+ font-style: normal;
+ font-weight: lighter;
+ font-size:1.35em;
+ line-height:inherit;
+ vertical-align: middle;
+ padding-left: .25em;
+ text-shadow:0 0 2px #fff;
+}
+#tour.browse i.fa.fa-camera:after{
+ content: "\f030";
+ color: #ccc;
+ float: right;
+ display: inline-block;
+}
+.hidden{
+ display:none;
+ visibility: hidden;
+}
+.admin-tour-browse-meta{
+ margin: .25em 0;
+ color:#777;
+}
+#tourbuilder-item-list{
+ padding-left: 0;
+}
+#tour-items-picker{
+ min-height: 600px;
+ min-height: 90vh;
+}
+#tour-item-search, #tour-item-exhibit-search{
+ padding: 10px;
+ width: 100%;
+ box-sizing: border-box;
+ margin: 0 !important;
+ border: 0px solid transparent;
+}
+.input-container{
+ background:linear-gradient(to bottom, #22546b, #102d3b);
+ padding: 1em !important;
+ box-sizing: border-box;
+}
+.exhibit-input-container{
+ margin-left: 3rem;
+ border: 1px solid #D6D5C2;
+ box-sizing: border-box;
+}
+.exhibit{
+ cursor: pointer;
+ margin: 0 1em;
+ flex: 0 0 80px;
+}
+.remove{
+ cursor: pointer;
+ flex: 0 0;
+}
+.tour-item-ui{
+ display: flex;
+ padding-bottom: 0.5em;
+}
+.tour-item-header{
+ display: flex;
+}
+.tour-item-title p{
+ margin: 0;
+}
+.exhibit-name {
+ margin-top: 0.2em;
+ color: #666;
+ font-style: italic;
+}
+
+.ui-menu-item{
+ padding-left: 5px;
+}
+
+#sortable { list-style-type: none; margin: 2em 0 0; padding: 0; width: 100%;}
+#sortable li {
+ display: flex;
+ padding: 0.5em 1em;
+ margin:0;
+ cursor: move; cursor: grab; cursor: -moz-grab; cursor: -webkit-grab;
+ flex-direction: column;
+}
+#sortable li span:first-child {flex-grow: 1;}
+.ui-state-highlight { line-height: 1.2em; color:#fff;background: linear-gradient(to bottom, #eaf2e1, #8eb763);text-shadow: -1px -1px 1px rgba(0,0,0,.5);}
+
+svg#drag{
+ margin-right: 1em;
+ vertical-align: middle;
+ margin-top: 0.4em;
+}
+
+.ui-state-highlight svg#drag{
+ fill-opacity:.55;
+}
+.ui-state-highlight svg#drag #top path{
+ fill:#fff;
+}
+.ui-state-highlight svg#drag #shadow path{
+ fill:#eaf2e1;
+}
\ No newline at end of file
diff --git a/views/admin/images/drag_icon.svg b/views/admin/images/drag_icon.svg
new file mode 100644
index 0000000..6a6b99f
--- /dev/null
+++ b/views/admin/images/drag_icon.svg
@@ -0,0 +1 @@
+
drag_icon
\ No newline at end of file
diff --git a/views/admin/images/featured-bg.png b/views/admin/images/featured-bg.png
new file mode 100644
index 0000000..8ac04c6
Binary files /dev/null and b/views/admin/images/featured-bg.png differ
diff --git a/views/admin/javascripts/walking-tour.js b/views/admin/javascripts/walking-tour.js
new file mode 100755
index 0000000..aa72e98
--- /dev/null
+++ b/views/admin/javascripts/walking-tour.js
@@ -0,0 +1,1029 @@
+jQuery(document).ready(function ($) {
+ var markers;
+ var map;
+ var markerData;
+ var key = "5b3ce3597851110001cf62489dde4c6690bc423bb86bd99921c5da77";
+ const markerFontHtmlStyles = `
+ transform: rotate(-45deg);
+ color:white;
+ text-align: center;
+ padding: 0.2rem 0 0.18rem 0;
+ font-size: 15px;
+ `
+
+ $('#map').css('height', 500);
+
+ var MAP_URL_TEMPLATE = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}';
+
+ var MAP_CENTER;
+ var MAP_ZOOM; // MAP_ZOOM controls the default zoom of the map
+ var MAP_MIN_ZOOM_STOP;
+ var MAP_MAX_ZOOM_STOP;
+ var LOCATE_BOUNDS;
+ var EXHIBIT_BUTTON_TEXT;
+ var DETAIL_BUTTON_TEXT;
+ var IS_AUTO_FIT;
+
+ var historicMapLayer;
+
+ var jqXhr;
+ var locationMarker;
+ var allItems = {};
+ var allMarkers = {};
+ // base url set to handle subdirectory installations
+ var baseUrl = window.location.origin;
+ var urlpaths = window.location.pathname.split("/");
+ if (urlpaths[1] != "admin") { baseUrl += "/" + urlpaths[1] };
+
+ /*
+ * JQuery Setup
+ */
+
+ // Check for user's first time visiting. Wait to locate the user after displaying tooltip on the first visit.
+ if (!($.cookie('myCookie'))) {
+ $('#first-time').show();
+ $('.tooltip-locate').toggle();
+ $.cookie('myCookie', 'visited', { path: '/', expires: 10000 });
+ }
+
+ $("#first-time > div.tooltip > button").on('click', function () {
+ $('.tooltip').fadeToggle();
+ $('.tooltip-locate').fadeToggle();
+ });
+
+ $("#first-time > div.tooltip-locate > button").on('click', function () {
+ $('#first-time').hide();
+ });
+
+ // Set up the dialog window.
+ $('#dialog').dialog({
+ autoOpen: false,
+ draggable: false,
+ resizable: false
+ });
+
+ // Handle the filter form.
+ $('#filter-button').click(function (e) {
+ e.preventDefault();
+
+ // First close any popups
+ var popupDiv = $('.leaflet-pane .leaflet-popup-pane')
+ if (popupDiv.children().length > 0) {
+ popupDiv.empty()
+ }
+
+ var filterButton = $(this);
+ var clicks = filterButton.data('clicks');
+ if (clicks) {
+ filterButton.removeClass('on').
+ find('.screen-reader-text').
+ html('Filters');
+ $('#filters').fadeToggle(200, 'linear');
+ } else {
+ filterButton.addClass('on').
+ find('.screen-reader-text').
+ html('Back to Map');
+ $('#filters').fadeToggle(200, 'linear');
+ }
+ filterButton.data('clicks', !clicks);
+ });
+
+ $('#tour-confirm-button').click(function (e) {
+ e.preventDefault();
+ var filterButton = $('#filter-button');
+ var tourSelected = []
+ var tourTypeCheck = $('input[name=place-type]:checked')
+ var curTourSelected;
+ var itemIDList = [];
+ var tour_id;
+
+ if (tourTypeCheck.length) {
+ tourTypeCheck.each(function () {
+ tour_id = this.value;
+ tourSelected.push(markerData[this.value].walkingPath);
+ curTourSelected = markerData[this.value];
+ });
+ } else {
+ var toursToPlot = Object.keys(markerData);
+ toursToPlot.forEach((ele) => {
+ tourSelected.push(markerData[ele].walkingPath);
+ });
+ }
+
+ filterButton.removeClass('on').
+ find('.screen-reader-text').
+ html('Filters');
+ $('#filters').fadeToggle(200, 'linear');
+
+ if (curTourSelected) {
+ curTourSelected.Data.features.forEach(ele => {
+ itemIDList.push(ele.properties.id)
+ })
+
+ $('#info-panel-container').fadeToggle(200, 'linear');
+ $('#toggle-map-button + .back-button').show();
+ populateTourIntroPopup(itemIDList, curTourSelected, tour_id);
+ }
+ let polylineGroup = L.featureGroup(tourSelected);
+ let bounds = polylineGroup.getBounds();
+ map.fitBounds(bounds);
+ })
+
+ // Revert form to default and display all markers.
+ $('#all-button').click(function (e) {
+ e.preventDefault();
+ revertFormState();
+ });
+
+ // Handle locate button.
+ $('#locate-button').click(function (e) {
+ e.preventDefault();
+ $(this).toggleClass('loading');
+ if (locationMarker) {
+ map.removeLayer(locationMarker)
+ locationMarker = null;
+ }
+ map.stopLocate();
+ map.locate({ watch: true });
+ });
+
+ // Toggle historic map layer on and off.
+ $('#toggle-map-button').click(function (e) {
+ e.preventDefault();
+ var toggleMapButton = $(this);
+ var clicks = toggleMapButton.data('clicks');
+ if (clicks) {
+ toggleMapButton.addClass('on');
+ toggleMapButton.find('.screen-reader-text').html('Map On');
+ map.addLayer(historicMapLayer);
+ } else {
+ if (historicMapLayer) {
+ toggleMapButton.removeClass('on');
+ toggleMapButton.find('.screen-reader-text').html('Map Off');
+ map.removeLayer(historicMapLayer);
+ }
+ }
+ toggleMapButton.data('clicks', !clicks);
+ });
+
+ // Toggle map filters
+ $('#filters div label').click(function () {
+ var checkboxLabel = $(this);
+ if (checkboxLabel.find('input[type=checkbox]').is(':checked')) {
+ checkboxLabel.addClass('on');
+ } else {
+ checkboxLabel.removeClass('on');
+ }
+ });
+
+ // Filter historic map layer.
+ $('#map-coverage').change(function () {
+ if (historicMapLayer) {
+ removeHistoricMapLayer();
+ }
+ if ('0' == $('#map-coverage').val()) {
+ $('#toggle-map-button').hide();
+ } else {
+ addHistoricMapLayer();
+ }
+ doFilters();
+ });
+
+ $('#map-coverage,#tour-type').on('touchstart touchend', function (event) {
+ event.stopPropagation();
+ });
+
+ // Filter place type.
+ $('input[name=place-type]').change(function () {
+ // allow only one box checked
+ $('input[name=place-type]:checked').not(this).prop('checked', false).
+ parent().removeClass('on');
+ // Handle all place types checkbox.
+ var placeTypeAll = $('input[name=place-type-all]');
+ if ($('input[name=place-type]:checked').length) {
+ placeTypeAll.prop('checked', false).parent().removeClass('on');
+ } else {
+ placeTypeAll.prop('checked', true).parent().addClass('on');
+ }
+ doFilters();
+ });
+
+ // Handle the all place types checkbox.
+ $('input[name=place-type-all]').change(function () {
+ // Uncheck all place types.
+ $('input[name=place-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+ doFilters();
+ });
+
+ // Filter event type.
+ $('input[name=event-type]').change(function () {
+ // Handle all event types checkbox.
+ var eventTypeAll = $('input[name=event-type-all]');
+ if ($('input[name=event-type]:checked').length) {
+ eventTypeAll.prop('checked', false).parent().removeClass('on');
+ } else {
+ eventTypeAll.prop('checked', true).parent().addClass('on');
+ }
+ doFilters();
+ });
+
+ // Handle the all event types checkbox.
+ $('input[name=event-type-all]').change(function () {
+ // Uncheck all event types.
+ $('input[name=event-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+ doFilters();
+ });
+
+ // Handle the info panel back button.
+ $('a.back-button').click(function (e) {
+ e.preventDefault();
+ $('#info-panel-container').fadeToggle(200, 'linear');
+ });
+
+ $(document).on('tourOrderChanged', async function (event, updatedOrder) {
+
+ async function getRoute(points) {
+ const coordinates = points.map(point => [point[1], point[0]]); // Convert to [lng, lat] format
+ const url = "https://api.openrouteservice.org/v2/directions/foot-walking/geojson";
+
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": key // Use the API key defined earlier
+ },
+ body: JSON.stringify({
+ coordinates: coordinates
+ })
+ });
+
+ if (!response.ok) {
+ console.error("OpenRouteService API error:", response.statusText);
+ return null;
+ }
+
+ const data = await response.json();
+ const route = data.features[0].geometry.coordinates.map(coord => [coord[1], coord[0]]); // Convert back to [lat, lng]
+ saveRoute(data);
+
+ return route;
+ } catch (error) {
+ console.error("Error querying OpenRouteService:", error);
+ return null;
+ }
+ }
+
+ if (markers) {
+ map.removeLayer(markers);
+ }
+
+ // Map the updated order to coordinates
+ const reorderedPoints = updatedOrder.map((id, index) => {
+ const feature = markerData[currentTour].Data.features.find(f => f.properties.id === id);
+ if (feature) {
+ // Update the marker label to reflect the new order
+ feature.properties.order = index + 1; // New order starts from 1
+ }
+ return feature ? [feature.geometry.coordinates[1], feature.geometry.coordinates[0]] : null;
+ }).filter(point => point !== null);
+
+ if (reorderedPoints.length < 2) {
+ console.error("At least two points are required to calculate a route.");
+ return;
+ }
+
+ // Query OpenRouteService for the new route
+ const route = await getRoute(reorderedPoints);
+
+ const reorderedPath = L.polyline(route, {
+ color: markerData[currentTour].Color || '#000000',
+ weight: 3,
+ opacity: 1,
+ smoothFactor: 1
+ });
+ markerData[currentTour].walkingPath = reorderedPath;
+
+ markers = new L.layerGroup();
+ markerData[currentTour].Data.features.forEach(feature => {
+ const latlng = [feature.geometry.coordinates[1], feature.geometry.coordinates[0]];
+ const numberIcon = L.divIcon({
+ className: "my-custom-pin",
+ iconSize: [25, 41],
+ iconAnchor: [12, 40],
+ popupAnchor: [0, -5],
+ html: `
+ ${feature.properties.order}
+ `
+ });
+ const marker = L.marker(latlng, { icon: numberIcon });
+ markers.addLayer(marker);
+ });
+
+ markers.addLayer(reorderedPath);
+ map.addLayer(markers);
+ });
+
+ /*
+ * Query backend
+ */
+
+
+ jqXhr = $.post(baseUrl + '/walking-tour/index/map-config', function (response) {
+ mapSetUp(response);
+ doQuery();
+ })
+
+ // Retain previous form state, if needed.
+ retainFormState();
+
+ function mapLocateCenter(map){
+ map.flyTo(MAP_CENTER, MAP_ZOOM);
+ }
+
+ /*
+ * Setup map layer
+ *
+ * Call only once during set up
+ */
+ function mapSetUp(response) {
+ EXHIBIT_BUTTON_TEXT = response['walking_tour_exhibit_button']
+ DETAIL_BUTTON_TEXT = response['walking_tour_detail_button']
+ MAP_ZOOM = parseInt(response["walking_tour_default_zoom"])
+ MAP_MAX_ZOOM_STOP = parseInt(response['walking_tour_max_zoom'])
+ MAP_MIN_ZOOM_STOP = parseInt(response['walking_tour_min_zoom'])
+ MAP_CENTER = parse1DArrayPoint("[" + response['walking_tour_center'] + ']')
+ IS_AUTO_FIT = response["walking_tour_auto_fit"] == '1'
+ // Set the base map layer.
+ var minZoom = MAP_ZOOM - MAP_MIN_ZOOM_STOP;
+ var maxZoom = MAP_ZOOM + MAP_MAX_ZOOM_STOP;
+ map = L.map('map', {
+ center: MAP_CENTER,
+ minZoom: minZoom,
+ maxZoom: maxZoom,
+ zoom: minZoom,
+ zoomControl: false
+ });
+ LOCATE_BOUNDS = map.getBounds();
+ map.setZoom(MAP_ZOOM);
+
+ map.addLayer(L.tileLayer(MAP_URL_TEMPLATE));
+ map.addControl(L.control.zoom({ position: 'topleft' }));
+ var extentControl = L.Control.extend({
+ options: {
+ position: 'topleft'
+ },
+ onAdd: function (map) {
+ var container = L.DomUtil.create('div', 'extentControl');
+ $(container).attr('id', 'extent-control');
+ $(container).css('width', '26px').css('height', '26px').css('outline', '1px black');
+ $(container).addClass('extentControl-disabled')
+ $(container).addClass('leaflet-bar')
+ $(container).on('click', function () {
+ mapLocateCenter(map);
+ });
+ return container;
+ }
+ })
+ map.addControl(new extentControl());
+ map.attributionControl.setPrefix('Tiles © Esri');
+
+ map.on('zoomend', function () {
+ if (map.getZoom() == minZoom) {
+ $('#extent-control').addClass('extentControl-disabled')
+ } else {
+ $('#extent-control').removeClass('extentControl-disabled')
+ }
+ })
+
+ // Handle location found.
+ map.on('locationfound', function (e) {
+ if (!locationMarker) {
+ $("#locate-button").toggleClass('loading');
+ }
+ // User within location bounds. Set the location marker.
+ if (L.latLngBounds(LOCATE_BOUNDS).contains(e.latlng)) {
+ if (locationMarker) {
+ // Remove the existing location marker before adding to map.
+ map.removeLayer(locationMarker);
+ } else {
+ // Pan to location only on first locate.
+ map.panTo(e.latlng);
+ }
+ locationMarker = L.marker(e.latlng, {
+ icon: L.icon({
+ iconUrl: 'plugins/WalkingTour/views/public/images/location.png',
+ iconSize: [25, 25]
+ })
+ });
+ locationMarker.addTo(map).bindPopup("You are within " + e.accuracy / 2 + " meters from this point");
+ // User outside location bounds.
+ } else {
+ var locateMeters = e.latlng.distanceTo(map.options.center);
+ var locateMiles = Math.ceil((locateMeters * 0.000621371) * 100) / 100;
+ alert('Cannot locate your location. You are ' + locateMiles + ' miles from the map bounds.');
+ map.stopLocate();
+ }
+ });
+
+ // Handle location error.
+ map.on('locationerror', function () {
+ $("#locate-button").toggleClass('loading');
+ map.stopLocate();
+ alert('Location Error, Please try again.');
+ console.log('location error')
+ });
+ }
+
+ // Save the route to the database
+ function saveRoute(route) {
+ $.ajax({
+ url: baseUrl + '/walking-tour/index/save-route',
+ method: 'POST',
+ data: {
+ tour_id: currentTour,
+ route: JSON.stringify(route)
+ },
+ success: function(response) {
+ console.log('Route saved to database');
+ },
+ error: function(xhr, status, error) {
+ console.error('Failed to save route:', error);
+ }
+ })
+ }
+
+ /*
+ * Query backend for tour info
+ *
+ * Call only once during set up
+ */
+ function doQuery() {
+ // correctly formats coordinates as [lat, long] (API returns [long, lat])
+ function orderCoords(path) {
+ var directions = [];
+ for (var i = 0; i < path.length; i++) {
+ directions.push([path[i][1], path[i][0]]);
+ }
+ return directions;
+ }
+
+ async function getOverallPath(points, key) {
+ var pointsParam = []
+ points.forEach(ele => {
+ pointsParam.push([ele.lng, ele.lat])
+ })
+ url = "https://api.openrouteservice.org/v2/directions/foot-walking/geojson"
+ const response = await fetch(url, {
+ method: "POST", // *GET, POST, PUT, DELETE, etc.
+ headers: {
+ 'Accept': 'application/json, application/geo+json, application/gpx+xml, img/png; charset=utf-8',
+ "Content-Type": "application/json",
+ 'Authorization': key
+ // 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: `{"coordinates": ${JSON.stringify(pointsParam)}}`, // body data type must match "Content-Type" header
+ })
+ return response.json();
+ }
+
+ var url;
+ var itemArray = []
+ var tourToItem = {}
+ var markerBounds = L.latLngBounds();
+ jqXhr = $.post(baseUrl + '/walking-tour/index/query', function (response) {
+ markerData = response;
+ dataArray = Object.entries(markerData)
+ for (const tour in markerData) {
+ itemArray = itemArray.concat(markerData[tour]['Data']['features'])
+ }
+ let requests = dataArray.map(([tourId, value]) => {
+ if (tourId != currentTour) return Promise.resolve();
+ return new Promise((resolve) => {
+ var numMarker = 1;
+ var response = value["Data"];
+ var itemIDList = [];
+
+ response.features.forEach(ele => {
+ itemIDList.push(ele.properties.id)
+ })
+ tourToItem[tourId] = itemIDList;
+ markerList = []
+ var geoJsonLayer = L.geoJson(response.features, {
+ // adds the correct number to each marker based on order of tour
+ pointToLayer: function (feature, latlng) {
+ var numberIcon = L.divIcon({
+ className: "my-custom-pin",
+ iconSize: [25, 41],
+ iconAnchor: [12, 40],
+ popupAnchor: [0, -5],
+ html: `
${numMarker}
`
+ });
+ numMarker++;
+ markerBounds.extend(latlng);
+ return L.marker(latlng, { icon: numberIcon });
+ },
+ onEachFeature: function (feature, layer) {
+ layer.on('click', function (e) {
+ // center click location
+ map.flyTo(e.latlng,MAP_ZOOM + MAP_MAX_ZOOM_STOP);
+ // Close the filtering
+ var filterButton = $('filter-button');
+ filterButton.removeClass('on').
+ find('.screen-reader-text').
+ html('Filters');
+ $('#filters').fadeOut(200, 'linear');
+
+ var marker = this;
+ //response = allItems[`${tourId}:${feature.properties.id}`]
+ //if (response == undefined) {
+ // $.post(baseUrl + '/walking-tour//index/get-item', { id: feature.properties.id, tour: tourId }, function (response) {
+ // allItems[`${tourId}:${feature.properties.id}`] = response;
+ // featureOnclickAction(response, layer, marker, itemIDList, value, tourId);
+ // })
+ //} else {
+ // featureOnclickAction(response, layer, marker, itemIDList, value, tourId);
+ //}
+
+ });
+
+ }
+ });
+ markerData[tourId].allMarker = markerList;
+ markerData[tourId].geoJson = geoJsonLayer;
+ var walkingPath = [];
+ var json_content = markerData[currentTour].Data.features;;
+ var pointList = [];
+ for (var i = 0; i < json_content.length; i++) {
+ lat = json_content[i].geometry.coordinates[1];
+ lng = json_content[i].geometry.coordinates[0];
+ var point = new L.LatLng(lat, lng);
+ pointList[i] = point;
+ }
+ getOverallPath(pointList, key).then((data) => {
+ saveRoute(data);
+
+ var path = data["features"][0]["geometry"]["coordinates"];
+ path = orderCoords(path);
+ for (var p of path) {
+ walkingPath.push(p);
+ }
+ var tourPolyline = new L.Polyline(walkingPath, {
+ color: value["Color"],
+ weight: 3,
+ opacity: 1,
+ smoothFactor: 1
+ });
+
+ markerData[tourId].walkingPath = tourPolyline;
+ resolve()
+ });
+ });
+ })
+ Promise.all(requests).then(() => {
+ createCustomCSS();
+ if (IS_AUTO_FIT){
+ map.fitBounds(markerBounds, {padding: [10, 10]})
+ mapLocateCenter = function(map) {
+ map.fitBounds(markerBounds, {padding: [10, 10]})
+ }
+ var curZoom = map._zoom;
+ map.setMaxZoom( curZoom + MAP_MAX_ZOOM_STOP);
+ map.setMinZoom( curZoom - MAP_MIN_ZOOM_STOP);
+ // map["options"]["minZoom"] = curZoom - MAP_MIN_ZOOM_STOP
+ }
+ doFilters();
+ });
+ });
+ }
+
+ /*
+ * Filter markers.
+ *
+ * This must be called on every form change.
+ */
+ function doFilters() {
+ // Remove the current markers.
+ if (markers) {
+ map.removeLayer(markers);
+ }
+
+ var mapCoverage = $('#map-coverage');
+ var tourTypeCheck = $('input[name=place-type]:checked')
+
+ var mapToPlot;
+ // Handle each filter
+ if ('0' != mapCoverage.val()) {
+ mapToPlot = mapCoverage.val();
+ }
+
+ var toursToPlot = [currentTour];
+
+ var pathToPlot = [];
+ var markerLayers = [];
+ var numMarkers = 0;
+
+ // handle the GeoJSON response, and add markers.
+ toursToPlot.forEach(ele => {
+ numMarkers += markerData[ele].Data.features.length;
+ markerLayers.push(markerData[ele].geoJson);
+ pathToPlot.push(markerData[ele].walkingPath);
+ markerData[ele].walkingPath.openPopup();
+ });
+ //response is an array of coordinate;
+ var item = (1 == numMarkers) ? 'item' : 'items';
+ $('#marker-count').text(numMarkers + " " + item);
+
+ try {
+ markers = new L.layerGroup();
+ }
+ catch (err) {
+ console.log(err)
+ }
+
+ markerLayers.forEach(ele => {
+ markers.addLayer(ele);
+ })
+ pathToPlot.forEach(ele => {
+ ele.addTo(markers);
+ })
+ map.addLayer(markers);
+ }
+
+ /*
+ * Setup color for each tour
+ *
+ */
+ function createCustomCSS() {
+ var style = document.createElement('style')
+ var css = ""
+ for (const tour_id in markerData) {
+ var color = markerData[tour_id]['Color']
+
+ if (color.length == 0){
+ color = "#000000"
+ }
+
+ var rgb = hexToRgb(color)
+ css += `#filters div label.label${tour_id}:before {
+ background-color: ${color} !important;
+ }
+ #filters div label.label${tour_id} {
+ background-color: rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.15) !important;
+ color: ${color} !important;
+ }
+ #filters div label.label${tour_id}.on:before {
+ background-color: rgba(33, 201, 0, 1) !important;
+ }\n`
+ }
+ style.innerHTML = css;
+ document.body.appendChild(style);
+ }
+
+ /*
+ * Setup popups for each item and tour
+ */
+
+ function featureOnclickAction(response, layer, marker, itemIDList, value, tourId) {
+ var popupContent = '' + response.title + ' ';
+ if (response.thumbnail) {
+ popupContent += '' + response.thumbnail + ' ';
+ }
+ popupContent += 'View More Info ';
+ if (!layer.getPopup()) {
+ marker.bindPopup(popupContent, { maxWidth: 200, offset: L.point(0, -40) }).openPopup();
+ allMarkers[response.id] = marker;
+ }
+
+ window.setTimeout(function () {
+ layer.getPopup().update()
+ $('.open-info-panel').click(function (e) {
+ e.preventDefault();
+ $('#info-panel-container').fadeToggle(200, 'linear');
+ $('#toggle-map-button + .back-button').show();
+ marker.closePopup();
+ });
+ }, 500);
+
+ // Populate the item info panel.
+ populatePopup(itemIDList, value, response, itemIDList.findIndex((ele) => ele == response.id), tourId);
+ }
+
+ function populateTourIntroPopup(itemIDList, value, tour_id) {
+ $('.next-button').unbind("click");
+ $('.prev-button').unbind("click");
+
+ $('.prev-button').addClass('off');
+ $('.next-button').addClass('off');
+
+ document.getElementById("info-panel-name").innerHTML = value["Tour Name"];
+ $('.panel-title').css("backgroundColor", value['Color'])
+ var content = $('#info-panel-content');
+ content.empty();
+
+ var infoContent = ""
+ var rightContent = "";
+ // click title to show the popup on map
+ if (value.Description != "") {
+ rightContent += '' + value.Description + '
'
+ } else {
+ rightContent += " No descriptions available.
"
+ }
+
+ if (value.Credits != "") {
+ rightContent += " Credits "
+ rightContent += '' + value.Credits + '
'
+ }
+ rightContent += 'Start Tour
';
+ window.setTimeout(function () {
+ $('#start-tour').click(function (e) {
+ var newId = itemIDList[0]
+ popupButtonEvent(e, newId, itemIDList, value, tour_id);
+ })
+ }, 500)
+ infoContent += '';
+
+ content.append('' + infoContent + '
')
+ }
+
+ function populatePopup(itemIDList, value, response, numPopup, tour_id) {
+ var numPopup = itemIDList.findIndex((ele) => ele == response.id);
+ console.log(value.Data.features);
+ console.log(itemIDList);
+ console.log(response.id);
+ var coor = value.Data.features[1].geometry.coordinates;
+ map.flyTo([coor[1], coor[0]], MAP_ZOOM + MAP_MAX_ZOOM_STOP);
+
+ $('.next-button').unbind("click");
+ $('.prev-button').unbind("click");
+
+ if (numPopup + 1 == itemIDList.length) {
+ $('.next-button').addClass('off');
+ } else {
+ $('.next-button').removeClass('off');
+ $('.next-button').unbind("click");
+ window.setTimeout(function () {
+ $('.next-button').click(function (e) {
+ var newId = itemIDList[numPopup + 1]
+ // value.allMarker[numPopup].closePopup();
+ popupButtonEvent(e, newId, itemIDList, value, tour_id);
+ })
+ }, 500)
+ }
+
+ if (numPopup - 1 == -1 || numPopup == -1) {
+ $('.prev-button').addClass('off');
+ } else {
+ $('.prev-button').removeClass('off');
+ window.setTimeout(function () {
+ $('.prev-button').click(function (e) {
+ var newId = itemIDList[numPopup - 1]
+ // value.allMarker[numPopup].closePopup();
+ popupButtonEvent(e, newId, itemIDList, value, tour_id);
+ })
+ }, 500)
+ }
+
+ document.getElementById("info-panel-name").innerHTML = value["Tour Name"] + ` #${numPopup + 1}`;
+ $('.panel-title').css("backgroundColor", value['Color'])
+ var content = $('#info-panel-content');
+ content.empty();
+
+ var infoContent = ""
+ var leftContent = "";
+ var rightContent = "";
+ if (response.fullsize) {
+ leftContent += response.fullsize;
+ infoContent += '' + leftContent + '
';
+ }
+
+ // click title to show the popup on map
+ rightContent += `` + response.title + ' '
+ if (response.abstract) {
+ rightContent += '' + response.abstract + '
';
+ } else if (response.description) {
+ rightContent += '' + response.description + '
';
+ } else {
+ rightContent += 'No descriptions available.
';
+ }
+ rightContent += ''
+ infoContent += '';
+
+ content.append('' + infoContent + '
')
+ }
+
+ function getMarkerHTML(color) {
+ let markerHtmlStyles = `
+ background-color: ${color};
+ width: 1.7rem;
+ height: 1.7rem;
+ display: block;
+ left: -0.5rem;
+ top: -0.5rem;
+ position: relative;
+ border-radius: 1.5rem 1.5rem 0;
+ transform: rotate(45deg);`
+ return markerHtmlStyles;
+ }
+
+ function popupButtonEvent(e, id, itemIDList, value, tour_id) {
+ e.preventDefault();
+ var response = allItems[`${tour_id}:${id}`]
+ if (response == undefined) {
+ $.post(baseUrl + '/walking-tour/index/get-item', { id: id, tour: tour_id }, function (response) {
+ allItems[`${tour_id}:${id}`] = response;
+ populatePopup(itemIDList, value, response, itemIDList.findIndex((ele) => ele == response.id), tour_id);
+ })
+ } else {
+ populatePopup(itemIDList, value, response, itemIDList.findIndex((ele) => ele == response.id), tour_id)
+ }
+ }
+
+
+ /*
+ * Add the historic map layer.
+ */
+ function addHistoricMapLayer() {
+ // Get the historic map data
+ var getData = { 'text': $('#map-coverage').val() };
+ $.get(baseUrl + '/walking-tour/index/historic-map-data', getData, function (response) {
+ historicMapLayer = L.tileLayer(
+ response.url,
+ { tms: true, opacity: 1.00 }
+ );
+ map.addLayer(historicMapLayer);
+ $('#toggle-map-button').show();
+
+ // Set the map title as the map attribution prefix.
+ map.attributionControl.setPrefix(response.title);
+ });
+ }
+
+ /*
+ * Remove the historic map layer.
+ */
+ function removeHistoricMapLayer() {
+ $('#toggle-map-button').data('clicks', false).hide();
+ map.removeLayer(historicMapLayer);
+ map.attributionControl.setPrefix('');
+ }
+
+ /*
+ * Helper Functions
+ */
+
+ function hexToRgb(hex) {
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16)
+ } : null;
+ }
+
+ function parse1DArrayPoint(text) {
+ ptn = text.split(",")
+ ptn.forEach(function (ele, index) {
+ temp_ele = ele.replace(/\s+/g, '');
+ temp_ele = temp_ele.replace('[', '');
+ temp_ele = temp_ele.replace(']', '');
+ this[index] = parseFloat(temp_ele)
+ }, ptn);
+ return ptn;
+ }
+
+ function parse2DArrayPoint(text) {
+ ptn = text.match(/(\[-?[0-9]*.[0-9]*, -?[0-9]*.[0-9]*\])/g)
+ b_new = []
+ ptn.forEach(ele => {
+ temp_ele = ele.split(",")
+ temp_ele.forEach(function (ele_inner, index_inner) {
+ temp_ele_inner = ele_inner.replace(/\s+/g, '');
+ temp_ele_inner = temp_ele_inner.replace('[', '');
+ temp_ele_inner = temp_ele_inner.replace(']', '');
+ this[index_inner] = parseFloat(temp_ele_inner)
+ }, temp_ele)
+ b_new.push(temp_ele)
+ })
+ return b_new
+ }
+
+ /*
+ * Revert to default (original) form state.
+ */
+ function revertFormState() {
+ if (historicMapLayer) {
+ removeHistoricMapLayer();
+ }
+
+ $('#map-coverage').val('0');
+ $('#tour-type').val('0');
+
+ $('#place-type-div').hide({ duration: 'fast' });
+ $('input[name=place-type-all]').prop('checked', true).
+ parent().addClass('on');
+ $('input[name=place-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+
+ $('#event-type-div').hide({ duration: 'fast' });
+ $('input[name=event-type-all]').prop('checked', true).
+ parent().addClass('on');
+ $('input[name=event-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+
+ doFilters();
+ }
+
+ /*
+ * Retain previous form state.
+ *
+ * Acts on the assumption that all browsers will preserve the form state
+ * when navigating back to the map from another page.
+ */
+ function retainFormState() {
+ if ('Place' == $('#tour-type').find(':selected').text()) {
+ var placeTypes = $('input[name=place-type]:checked');
+ if (placeTypes.length) {
+ $('input[name=place-type-all]').parent().removeClass('on');
+ placeTypes.parent().addClass('on');
+ }
+ $('#place-type-div').show({ duration: 'fast' });
+ }
+ if ('Event' == $('#tour-type').find(':selected').text()) {
+ var eventTypes = $('input[name=event-type]:checked');
+ if (eventTypes.length) {
+ $('input[name=event-type-all]').parent().removeClass('on');
+ eventTypes.parent().addClass('on');
+ }
+ $('#event-type-div').show({ duration: 'fast' });
+ }
+ }
+
+ /*
+ * Revert to default (original) form state.
+ */
+ function revertFormState() {
+ if (historicMapLayer) {
+ removeHistoricMapLayer();
+ }
+
+ $('#map-coverage').val('0');
+ $('#tour-type').val('0');
+
+ $('#place-type-div').hide({ duration: 'fast' });
+ $('input[name=place-type-all]').prop('checked', true).
+ parent().addClass('on');
+ $('input[name=place-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+
+ $('#event-type-div').hide({ duration: 'fast' });
+ $('input[name=event-type-all]').prop('checked', true).
+ parent().addClass('on');
+ $('input[name=event-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+
+ doFilters();
+ }
+
+ /*
+ * Retain previous form state.
+ *
+ * Acts on the assumption that all browsers will preserve the form state
+ * when navigating back to the map from another page.
+ */
+ function retainFormState() {
+ if ('Place' == $('#tour-type').find(':selected').text()) {
+ var placeTypes = $('input[name=place-type]:checked');
+ if (placeTypes.length) {
+ $('input[name=place-type-all]').parent().removeClass('on');
+ placeTypes.parent().addClass('on');
+ }
+ $('#place-type-div').show({ duration: 'fast' });
+ }
+ if ('Event' == $('#tour-type').find(':selected').text()) {
+ var eventTypes = $('input[name=event-type]:checked');
+ if (eventTypes.length) {
+ $('input[name=event-type-all]').parent().removeClass('on');
+ eventTypes.parent().addClass('on');
+ }
+ $('#event-type-div').show({ duration: 'fast' });
+ }
+ }
+
+ function hexToRgb(hex) {
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16)
+ } : null;
+ }
+});
+
diff --git a/views/admin/tours/add.php b/views/admin/tours/add.php
new file mode 100644
index 0000000..ed96d43
--- /dev/null
+++ b/views/admin/tours/add.php
@@ -0,0 +1,49 @@
+ 'Add Tour', 'content_class' => 'vertical-nav',
+ 'bodyclass' => 'tours primary add-tour-form' ) );
+echo flash();
+?>
+
+
+
+
diff --git a/views/admin/tours/browse.php b/views/admin/tours/browse.php
new file mode 100644
index 0000000..a34b229
--- /dev/null
+++ b/views/admin/tours/browse.php
@@ -0,0 +1,92 @@
+ 'add' ) );
+
+echo head( array( 'title' => $pageTitle, 'bodyid'=>'tour','bodyclass' => 'tours browse' ) );
+echo flash();
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+ ID
+ Title
+
+ Edit?
+
+
+
+
+
+ 'show','id' => $tour->id ), 'tourAction' );
+ $editUrl = url( array( 'action' => 'edit','id' => $tour->id ), 'tourAction' );
+ ?>
+
+
+ id; ?>
+ featured) ? 'class="featured"' : null?>>
+
+ title; ?>
+
+ Locations : '.count($tour->Items).( metadata( $tour, 'Credits' ) ? ' · Credits : '.metadata( $tour, 'Credits' ).'' : null ).'Public : '.( (metadata( $tour, 'Public' ) == 1) ? 'Yes' : 'No' ).' · Featured : '.( (metadata( $tour, 'Featured' ) == 1) ? 'Yes' : 'No' ).'';?>
+
+ hasImage() ){ echo ' ';} ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/views/admin/tours/edit.php b/views/admin/tours/edit.php
new file mode 100644
index 0000000..9e7363a
--- /dev/null
+++ b/views/admin/tours/edit.php
@@ -0,0 +1,69 @@
+ $tourTitle, 'content_class' => 'vertical-nav',
+ 'bodyclass' => 'edit','bodyid'=>'tour' ) );
+echo flash();
+?>
+
+
+
+
diff --git a/views/admin/tours/form.php b/views/admin/tours/form.php
new file mode 100644
index 0000000..dcab5ab
--- /dev/null
+++ b/views/admin/tours/form.php
@@ -0,0 +1,347 @@
+
+
+drag_icon ';
+?>
+
+
+
diff --git a/views/admin/tours/show.php b/views/admin/tours/show.php
new file mode 100644
index 0000000..2661846
--- /dev/null
+++ b/views/admin/tours/show.php
@@ -0,0 +1,112 @@
+ $tourTitle,
+ 'bodyclass' => 'show','bodyid'=>'tour' ) );
+echo flash();
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Postscript Text
+
+ '.htmlspecialchars_decode(metadata( 'tour', 'postscript_text' )).''; ?>
+
+
+
+
+
+ getItems();
+if( $tour->getItems() ): ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'delete-confirm big red button' ),
+ 'delete-confirm' ); ?>
+
+
+
+
+
+
+ :
+
+ public) ? __('Yes') : __('No'); ?>
+
+
+
+ :
+
+ featured) ? __('Yes') : __('No'); ?>
+
+
+
+
+
diff --git a/views/public/css/MarkerCluster.Default.css b/views/public/css/MarkerCluster.Default.css
old mode 100644
new mode 100755
diff --git a/views/public/css/MarkerCluster.Default.ie.css b/views/public/css/MarkerCluster.Default.ie.css
old mode 100644
new mode 100755
diff --git a/views/public/css/MarkerCluster.css b/views/public/css/MarkerCluster.css
old mode 100644
new mode 100755
diff --git a/views/public/css/icons.png b/views/public/css/icons.png
new file mode 100644
index 0000000..d8a0abd
Binary files /dev/null and b/views/public/css/icons.png differ
diff --git a/views/public/css/loading-gif.gif b/views/public/css/loading-gif.gif
new file mode 100644
index 0000000..4301102
Binary files /dev/null and b/views/public/css/loading-gif.gif differ
diff --git a/views/public/css/mall-map.css b/views/public/css/mall-map.css
deleted file mode 100644
index b081359..0000000
--- a/views/public/css/mall-map.css
+++ /dev/null
@@ -1,410 +0,0 @@
-@font-face {
- font-family: 'icomoon';
- src:url('../fonts/icomoon.eot');
- src:url('../fonts/icomoon.eot?#iefix') format('embedded-opentype'),
- url('../fonts/icomoon.woff') format('woff'),
- url('../fonts/icomoon.ttf') format('truetype'),
- url('../fonts/icomoon.svg#icomoon') format('svg');
- font-weight: normal;
- font-style: normal;
-}
-
-* {
- box-sizing: border-box;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- -o-box-sizing: border-box;
-}
-
-a#locate-button:before, #filter-button:before,
-#filters div label.on:before, #toggle-map-button,
-.back-button:before{
- font-family: 'icomoon';
- speak: none;
- font-weight: normal;
- font-variant: normal;
- text-transform: none;
- line-height: 1;
- -webkit-font-smoothing: antialiased;
-}
-
-html, body {
- height: 100%;
-}
-
-body {
- padding: 0;
- margin: 0;
- position: relative;
- font-family: "Raleway", sans-serif;
- color: #375e5d;
-}
-
-#map {
- width: 100%;
-}
-
-h1 {
- margin-top: 0;
- font-weight: normal;
-}
-
-img {
- margin: 20px 0;
-}
-
-select {
- width: 100%;
- margin-bottom: 1.5em;
- font-size: 1em;
-}
-
-label {
- display: inline-block;
- margin-bottom: .5em;
-}
-
-#site-title img {
- margin: 0;
-}
-
-/* header {
- width: 100%;
- height: 59px;
- display: block;
- position: relative;
- top: 0;
- background-color: rgba(255, 255, 255, 0.8);
- left: 0;
- z-index: 1020;
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
- background-image: linear-gradient(90deg, rgba(255, 194, 0, 0.6), rgba(190, 183, 0, 0.35));
-} */
-
-.screen-reader-text {
- display: none;
-}
-
-#marker-count, #all-button {
-}
-
-div[role="main"] {
- width: 100%;
- position: relative;
- overflow: hidden;
-}
-
-.button {
- color: #fff;
- background-color: #2b89d9;
- padding: 12px;
- font-size: 16px;
- line-height: 100%;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- text-decoration: none;
- border-radius: 3px;
-}
-
-#marker-count {
-}
-
-#all-button { right: 60px; }
-
-#filters h1 {
- margin-top: 0;
- font-weight: normal;
- background-color: #FFE400;
- position: absolute;
- top: 0;
- left: 0;
- padding: 10px;
- width: 100%;
-}
-
-#info-panel h1 {
- background-color: #FFE400;
- margin: -20px -10px 20px;
- padding: 10px;
-}
-
-a#toggle-map-button:before { content: "\e00f"; }
-
-a#locate-button:before {
- content: "\e012";
- background-color: rgba(255, 255, 255, 0.8);
- width: 100%;
- height: 100%;
- display: block;
- padding: 7px;
- border-radius: 7px;
-}
-
-a#locate-button {
- position: absolute;
- bottom: 11px;
- z-index: 1000;
- text-decoration: none;
- color: #fff;
- background-color: #2B89D9;
- -webkit-box-shadow: 0 0 20px rgba(0,0,0,.15);
- -moz-box-shadow: 0 0 20px rgba(0,0,0,.15);
- box-shadow: 0 0 20px rgba(0,0,0,.15);
- font-size: 20px;
- line-height: 100%;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- border: 1px solid #78B6EB;
- border-radius: 3px;
- overflow: hidden;
-}
-
-a#locate-button {
- left: 10px;
-}
-
-.touch a#locate-button {
- border-radius: 10px;
- border: 4px solid rgba(0,0,0,.3) !important;
-}
-
-.touch a#locate-button.disabled:before {
- background-color: rgba(255,255,255.8);
-}
-
-#filter-button:before { content: "\e013"; }
-
-#filter-button.on:before, a.back-button:before { content: "\e00d"; }
-
-#filter-button,
-#toggle-map-button {
- position: absolute;
- top: 0;
- font-size: 24px;
- height: 2.25em;
- line-height: 2.25em;
- padding: 0 10px;
- color: #fff;
- border-left: 1px solid rgba(255,255,255,.15);
- cursor: pointer;
-}
-
-#filter-button {
- right: 0;
-}
-
-#toggle-map-button {
- right: 45px;
- opacity: .25;
- border-left: 1px solid rgba(255,255,255,.65);
-}
-
-#toggle-map-button.on {
- opacity: 1;
- border-left: 1px solid rgba(255,255,255,.15);
-}
-
-#info-panel .back-button {
- display: block;
- padding: .5em 10px;
- z-index: 1010;
- background-color: #FFE400;
- border-bottom: 1px solid rgba(255,255,255,.5);
-}
-
-#filters {
- position: absolute;
- left: 0;
- top: 54px;
- height: 100%;
- background: rgba(255, 255, 255, 0.9);
- background-size: cover;
- width: 100%;
- z-index: 1001;
- padding: 80px 10px 20px;
- overflow-y: auto;
- -webkit-overflow-scrolling: touch;
- display: none;
-}
-
-#filters div label {
- background-color: rgba(219, 108, 54, 0.15);
- border-radius: 3px;
- cursor: pointer;
- vertical-align: middle;
- line-height: 40px;
- width: 100%;
- color: #DF582D;
- position: relative;
-}
-
-#filters div label.on {
- background-color: rgba(33, 201, 0, 0.15);
- color: #36A036;;
-}
-
-#filters div label:before {
- content: "";
- background-color: #DB6C36;
- color: #FFF;
- height: 40px;
- display: block;
- float: left;
- vertical-align: middle;
- line-height: 40px !important;
- width: 40px;
- text-align: center;
- border-radius: 3px 0 0 3px;
- margin-right: .5em;
-}
-
-#filters div label.on:before {
- content: "\e006";
- background-color: rgba(33, 201, 0, 1);
-}
-
-#filters div input[type=checkbox] {
- margin: 0 2em 0 -2em;
- display: none;
-}
-
-.disabled {
- background-color: rgba(0, 0, 0, 0) !important;
- color: #C7C7C7 !important;
- border: 1px solid rgba(0, 0, 0, 0.3) !important;
- border-radius: 10px !important;
- box-sizing: content-box !important;
-}
-
-#info-panel {
- display: none;
- position: absolute;
- background: url('../images/bg.jpg') no-repeat top right;
- background-size: cover;
- top: 0;
- left: 0;
- height: 100%;
- z-index: 1020;
- overflow-y: auto;
- -webkit-overflow-scrolling: touch;
- width: 100%;
- margin-bottom: 1em;
-}
-
-#info-panel-content {
- padding: 20px 10px;
-}
-
-#info-panel p {
- line-height: 24px;
-}
-
-/* Leaflet Overrides */
-
-.leaflet-container {
- font-family : "Raleway", sans-serif !important;
-}
-
-.leaflet-popup-content-wrapper, .leaflet-popup-tip {
- border-radius: 0;
-}
-
-.leaflet-container a.leaflet-popup-close-button {
- position: absolute;
- top: -6px !important;
- right: -10px !important;
- padding: 1px 2px !important;
- width: 20px !important;
- height: 20px !important;
- color: #FFF !important;
- background: #DB6C36 !important;
- border-radius: 20px;
- border: 1px solid #F89361;
- font-weight: normal !important;
-}
-
-.leaflet-popup-content {
- text-align: center;
- margin: 10px !important;
-}
-
-.leaflet-popup-content h3 {
- font-weight: normal;
- margin-bottom: 0;
-}
-
-.leaflet-popup-content img {
- margin: 10px auto;
-}
-
-.open-info-panel.button {
- width: 100%;
- display: block;
- color: #FFF;
- text-align: center;
- font-size: 14px;
- padding: 10px;
-}
-
-/* Tooltip styles */
-
-#first-time {
- position: absolute;
- top: 3.3125em;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 9999;
- display: none;
-}
-
-#first-time .overlay {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0,0,0,.75);
-}
-
-#first-time .tooltip {
- background-color: #fff;
- position: absolute;
- max-width: 50%;
- right: 20px;
- top: 1.5em;
- padding: 0 20px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- border-radius: 5px;
-}
-
-#first-time .tooltip:before {
- content: "";
- width: 10px;
- height: 20px;
- position: absolute;
- top: -10px;
- right: 0;
- border-right: 10px solid #FFF;
- border-top: 10px solid rgba(0, 0, 0, 0);
-}
-
-#first-time button {
- border: 0;
- -webkit-border-radius: 0px;
- -moz-border-radius: 0px;
- border-radius: 0;
- margin-bottom: 1.5em;
-}
-
-@media screen and (max-width: 640px) {
-
- #first-time .tooltip {
- max-width: 90%;
- right: 5%;
- }
-
-}
\ No newline at end of file
diff --git a/views/public/css/marker-icon.png b/views/public/css/marker-icon.png
new file mode 100644
index 0000000..3573aa0
Binary files /dev/null and b/views/public/css/marker-icon.png differ
diff --git a/views/public/css/sphere.svg b/views/public/css/sphere.svg
new file mode 100644
index 0000000..d7a6edf
--- /dev/null
+++ b/views/public/css/sphere.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/views/public/css/walking-tour.css b/views/public/css/walking-tour.css
new file mode 100644
index 0000000..fec0976
--- /dev/null
+++ b/views/public/css/walking-tour.css
@@ -0,0 +1,827 @@
+@font-face {
+ font-family: 'icomoon';
+ src:url('../fonts/icomoon.eot');
+ src:url('../fonts/icomoon.eot?#iefix') format('embedded-opentype'),
+ url('../fonts/icomoon.woff') format('woff'),
+ url('../fonts/icomoon.ttf') format('truetype'),
+ url('../fonts/icomoon.svg#icomoon') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+
+* {
+ box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -o-box-sizing: border-box;
+}
+
+a#locate-button:before, #filter-button:before,
+#filters div label.on:before, #toggle-map-button, .extentControl.leaflet-control:before,
+.back-button:before{
+ font-family: 'icomoon';
+ speak: none;
+ font-weight: normal;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+}
+a{
+ color: black;
+}
+html, body {
+ height: 100%;
+}
+
+body {
+ padding: 0;
+ margin: 0;
+ position: relative;
+}
+
+#map {
+ top:0;
+ bottom:0;
+ width:100%;
+ height: 40em !important;
+}
+
+h1 {
+ margin-top: 0;
+ font-weight: normal;
+}
+
+img {
+ margin: 20px 0;
+}
+
+select {
+ width: 100%;
+ margin-bottom: 1.5em;
+ font-size: 1em;
+}
+
+label {
+ display: inline-block;
+ margin-bottom: .5em;
+}
+
+#site-title img {
+ margin: 0;
+}
+
+/* header {
+ width: 100%;
+ height: 59px;
+ display: block;
+ position: relative;
+ top: 0;
+ background-color: rgba(255, 255, 255, 0.8);
+ left: 0;
+ z-index: 1020;
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
+ background-image: linear-gradient(90deg, rgba(255, 194, 0, 0.6), rgba(190, 183, 0, 0.35));
+} */
+
+/*
+ padding: 0.375em 0;
+ background-color: #8cbcd8;
+ color: #fff;
+ margin: 0;
+ font-size: 24px;
+ line-height: 36px;
+ text-shadow: 0.5px 0.5px 2px black;
+ text-align: center;
+*/
+.screen-reader-text {
+ display: none;
+}
+
+#marker-count, #all-button {
+}
+
+div[role="main"] {
+ width: 100%;
+ position: relative;
+ overflow: hidden;
+}
+
+:root {
+ --accent-color: #8cbcd8;
+ }
+
+.button {
+ color: #fff;
+ background-color: var(--accent-color);
+ padding: 12px;
+ font-size: 16px;
+ line-height: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ text-decoration: none;
+ border-radius: 3px;
+}
+
+a.button{
+ background-color: #3d3d3d;
+ color: #fff;
+ padding: 0.75em;
+ border-radius: 3px;
+ border-radius: 3px;
+ border-radius: 3px;
+}
+
+
+#marker-count {
+}
+
+#all-button { right: 60px; }
+
+#filters h1 {
+ margin-top: 0;
+ font-weight: normal;
+ background-color: #edc86a;
+ color: #3d5a80;
+ position: absolute;
+ top: 0;
+ left: 0;
+ padding: 10px;
+ width: 100%;
+}
+
+#info-panel h1 {
+ color: white;
+ margin: 0;
+ padding: 15px;
+ padding-top: 20px;
+ font-weight: 600;
+ text-align: center;
+ z-index: 4000;
+ font-size: 2rem;
+ margin: auto;
+ text-shadow: 0.5px 0.5px 2px black;
+}
+
+a#toggle-map-button:before { content: "\e00f"; }
+
+a#locate-button:before {
+ content: "\e012";
+ width: 100%;
+ height: 100%;
+ display: inline-block;
+ padding: 7px;
+ border-radius: 7px;
+}
+
+a#locate-button.loading:before {
+ content: url(loading-gif.gif);
+ transform: scale(0.1) translateY(-300%) translateX(-300%);
+}
+
+a#locate-button, a#locate-button.loading {
+ position: absolute;
+ height: 35px;
+ width: 35px;
+ bottom: 11px;
+ z-index: 100000;
+ text-decoration: none;
+ color: black;
+ background-color: white;
+ -webkit-box-shadow: 0 1px 5px rgba(0,0,0,0.65);
+ -moz-box-shadow: 0 1px 5px rgba(0,0,0,0.65);
+ box-shadow: 0 1px 5px rgba(0,0,0,0.65);
+ font-size: 20px;
+ line-height: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+a#locate-button {
+ left: 10px;
+}
+
+.touch a#locate-button {
+ border-radius: 10px;
+ /* border: 5px solid rgba(0,0,0,.3) !important; */
+}
+
+.touch a#locate-button.disabled:before {
+ background-color: rgba(255,255,255.8);
+}
+
+.map-title{
+ display: flex;
+ padding: 0.05em 0;
+ background-color: var(--accent-color);
+ color: #fff;
+ font-size: 24px;
+ line-height: 36px;
+ text-shadow: 0.5px 0.5px 2px black;
+ text-align: center;
+ justify-content: space-between;
+ align-items: center;
+ height: 3em;
+}
+
+.map-title #marker-count {
+ margin: 0;
+ padding: 0;
+ flex: 1;
+ text-align: center;
+}
+
+.map-title #filter-button {
+ flex: 0;
+ text-align: right;
+}
+
+#filter-button:before { content: "\e013"; }
+
+#filter-button.on:before, a.back-button:before { content: "\e00d"; }
+
+#filter-button,
+#toggle-map-button {
+ position: absolute;
+ top: 0;
+ font-size: 24px;
+ height: 3em;
+ line-height: 2.25em;
+ padding: 10px;
+ color: #fff;
+ border-left: 1px solid rgba(255,255,255,.15);
+ cursor: pointer;
+}
+
+#filter-button {
+ right: 0;
+}
+
+.tour-filter{
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 0 0 0.5rem 0;
+}
+
+.tour-filter #tour-confirm-button {
+ background-color: var(--accent-color);
+ color: #fff;
+ padding: 10px;
+ margin-right: 1.3rem;
+ border-radius: 3px;
+}
+
+#toggle-map-button {
+ right: 45px;
+ opacity: .25;
+ border-left: 1px solid rgba(255,255,255,.65);
+}
+
+#toggle-map-button.on {
+ opacity: 1;
+ border-left: 1px solid rgba(255,255,255,.15);
+}
+
+#info-panel .back-button {
+ display: block;
+ padding: .5em 10px;
+ z-index: 1010;
+ color: white;
+ text-shadow: 0.5px 0.5px 2px black;
+
+}
+
+#info-panel .back-button:hover {
+ color: #edc86a;
+}
+
+.panel-title a {
+ display: block;
+ margin: 25px;
+ width: 40px;
+ height: 40px;
+ /* display: block;
+ margin: 25px;
+ width: 40px;
+ height: 40px;
+ border-top: 5px solid white;
+ border-left: 5px solid white;
+ filter: drop-shadow(2px 2px 2px black); */
+}
+
+#info-panel .next-button:before {
+ content: "\203A";
+ font-size: 7rem;
+ color: white;
+ filter: drop-shadow(2px 2px 2px black);
+}
+
+#info-panel .prev-button:before {
+ content: "\2039";
+ font-size: 7rem;
+ color: white;
+ filter: drop-shadow(2px 2px 2px black);
+}
+
+#info-panel .prev-button.off:before,
+#info-panel .next-button.off:before {
+ content: "";
+}
+
+#info-panel .prev-button.off,
+#info-panel .next-button.off {
+ cursor: default;
+}
+
+#info-panel a {
+ border: none;
+}
+
+#tour-type-div p{
+ margin: 0;
+ font-size: 20px;
+}
+
+#filters {
+ position: absolute;
+ left: 0;
+ height: 100%;
+ background: rgba(255, 255, 255, 0.9);
+ background-size: cover;
+ width: 100%;
+ z-index: 1001;
+ padding: 10px 20px;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ display: none;
+}
+
+#filters div label {
+ background-color: rgba(219, 108, 54, 0.15);
+ border-radius: 3px;
+ cursor: pointer;
+ vertical-align: middle;
+ line-height: 40px;
+ width: 100%;
+ color: #DF582D;
+ position: relative;
+ font-weight: 550;
+ letter-spacing: 0.5px;
+}
+
+#filters div label.on {
+ background-color: rgba(33, 201, 0, 0.15) !important;
+ color: #36A036 !important;
+}
+
+#filters div label:before {
+ content: "";
+ background-color: #DB6C36;
+ color: #FFF;
+ height: 40px;
+ display: block;
+ float: left;
+ vertical-align: middle;
+ line-height: 40px !important;
+ width: 40px;
+ text-align: center;
+ border-radius: 3px 0 0 3px;
+ margin-right: .5em;
+}
+
+#filters div label.on:before {
+ content: "\ea10";
+ background-color: rgba(33, 201, 0, 1) !important;
+}
+
+#filters div input[type=checkbox] {
+ margin: 0 2em 0 -2em;
+ display: none;
+}
+
+.disabled {
+ background-color: rgba(0, 0, 0, 0) !important;
+ color: #C7C7C7 !important;
+ border: 1px solid rgba(0, 0, 0, 0.3) !important;
+ border-radius: 10px !important;
+ box-sizing: content-box !important;
+}
+#info-panel-container {
+ width: 100%;
+ border: 48px solid transparent;
+ background: rgba(0,0,0,.5);
+ z-index: 2000;
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+}
+
+#info-panel{
+ position: absolute;
+ width: 800px;
+ height: 600px;
+ max-width: 100%;
+ max-height: 100%;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ margin: auto;
+ -webkit-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
+ z-index: inherit;
+}
+
+#info-panel-content {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ color: #222;
+ font-size: 16px;
+ overflow: hidden;
+ background: url('../images/bg.jpg') no-repeat top right;
+ background-size: cover;
+}
+
+#info-panel p {
+ width: 100%;
+ line-height: 1.5;
+ white-space: pre-line;
+ word-break: break-word;
+}
+
+.info-content{
+ display: flex;
+ width: 100%;
+ height: 100%;
+ color: #222;
+ overflow: hidden;
+}
+
+.popupButton{
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ font-size: 0.5rem;
+ gap: 20px;
+}
+
+@media screen and (max-width: 958px) {
+ .tour-filter #tour-confirm-button {
+ margin: 0;
+ }
+}
+
+@media screen and (max-width: 640px) {
+ .map-title{
+ padding: 0.375em 2.85714%;
+ height: 2.25em;
+ }
+
+ .map-title #marker-count{
+ flex: 0 1 auto;
+ }
+
+ .map-title #filter-button{
+ height: 2.25em;
+ padding: 0 10px;
+ }
+
+ .info-content {
+ flex-direction: column;
+ }
+ #info-panel{
+ height: 840px;
+ }
+ #info-panel-container{
+ border-left-width: 16px;
+ border-right-width: 16px;
+ border-bottom-width: 16px;
+ }
+ #info-panel h1{
+ font-size: 1.5rem;
+ padding: 0;
+ line-height: 40px;
+ }
+ .info-content .image-container{
+ height: 25%;
+ }
+ h2.info-panel-title{
+ font-size: 1rem;
+ }
+ .panel-title{
+ justify-content: center;
+ padding: 5px 0;
+ }
+ .panel-title a{
+ width: 25px;
+ height: 25px;
+ }
+ #info-panel .next-button:before{
+ font-size: 5rem;
+ }
+ #info-panel .prev-button:before{
+ font-size: 5rem;
+ }
+}
+
+.info-content .image-container{
+ flex: 1 1;
+ position: relative;
+ margin: 0;
+}
+
+.info-content .image-container img{
+ position: static;
+ width: 100%;
+ height: 100% !important;
+ object-fit: cover;
+ margin: 0;
+}
+.info-content .content-container{
+ flex: 1 1;
+ display: flex;
+ flex-direction: column;
+ overflow: auto;
+}
+.info-content .content-container .article{
+ flex: none;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding: 0px 32px;
+ padding-bottom: 20px;
+}
+h2.info-panel-title{
+ width: 100%;
+ font-size: 1.5rem;
+ font-weight: 700;
+ line-height: 1.33;
+ margin: 12px 0;
+ margin-top: 1.45rem;
+}
+h2.credits{
+ width: 100%;
+ font-size: 1.5rem;
+ font-weight: 700;
+ line-height: 1.33;
+ margin: 0;
+}
+.panel-title{
+ display: flex;
+ justify-content: space-between;
+ background-color: var(--accent-color);
+}
+
+/* Leaflet Overrides */
+
+.extentControl{
+ background : url(sphere.svg) no-repeat 50% 50%;
+ background-size: 20px 20px;
+ box-shadow: 0 1px 5px rgba(0,0,0,0.65);
+ border-radius: 4px;
+ cursor: pointer !important;
+ background-color: #fff;
+}
+
+.extentControl-disabled{
+ background-color: #f4f4f4;
+ color: #bbb;
+ cursor: default !important;
+}
+
+.leaflet-container {
+ font-family : "Raleway", sans-serif !important;
+}
+
+.leaflet-tile-pane {
+ margin-top: -15px;
+}
+
+.leaflet-popup-content-wrapper, .leaflet-popup-tip {
+ border-radius: 0;
+}
+
+.leaflet-control-zoom a:link {
+ color: #3d5a80;
+}
+
+.leaflet-control-zoom a{
+ color: black !important;
+}
+
+.leaflet-bar a.leaflet-disabled{
+ color: #bbb !important;
+}
+
+.leaflet-container a.leaflet-popup-close-button {
+ position: absolute;
+ top: -6px !important;
+ right: -10px !important;
+ padding: 1px 2px !important;
+ width: 20px !important;
+ height: 20px !important;
+ color: #FFF !important;
+ background: #DB6C36 !important;
+ border-radius: 20px;
+ border: 1px solid #F89361;
+ font-weight: normal !important;
+}
+
+.leaflet-popup-content {
+ text-align: center;
+ margin: 10px !important;
+}
+
+.leaflet-popup-content h3 {
+ font-weight: normal;
+ margin-bottom: 0;
+}
+
+.leaflet-popup-content img {
+ margin: 10px auto;
+}
+
+.open-info-panel.button {
+ width: 100%;
+ display: block;
+ color: #FFF;
+ text-align: center;
+ font-size: 14px;
+ padding: 10px;
+}
+
+/* Tooltip styles */
+
+#first-time {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 9999;
+ display: none;
+}
+
+#first-time .overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0,0,0,.75);
+}
+
+#first-time .tooltip {
+ background-color: #fff;
+ position: absolute;
+ max-width: 50%;
+ right: 20px;
+ top: 1.5em;
+ padding: 0 20px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+#first-time .tooltip:before {
+ content: "";
+ width: 10px;
+ height: 20px;
+ position: absolute;
+ top: -10px;
+ right: 0;
+ border-right: 10px solid #FFF;
+ border-top: 10px solid rgba(0, 0, 0, 0);
+}
+
+/* #first-time .tooltip-locate {
+ background-color: #fff;
+ position: absolute;
+ max-width: min(420px, calc(100% - 72px));
+ left: 56px;
+ right: auto;
+ bottom: 56px;
+ padding: 0 20px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+#first-time .tooltip-locate:before {
+ content: "";
+ width: 10px;
+ height: 20px;
+ position: absolute;
+ top: auto;
+ bottom: 14px;
+ left: -10px;
+ transform: rotate(180deg);
+ border-right: 10px solid #FFF;
+ border-top: 10px solid rgba(0, 0, 0, 0);
+} */
+
+#first-time .tooltip-locate {
+ background-color: #fff;
+ position: fixed;
+
+ width: 360px;
+ max-width: calc(100vw - 32px);
+
+ padding: 18px 22px;
+ border-radius: 5px;
+
+ z-index: 10000;
+ /*
+ * JS will dynamically set left/top based on #locate-button.
+ */
+ left: 0;
+ top: 0;
+ /*
+ * Place the tooltip above the locate icon.
+ */
+ transform: translateY(calc(-100% - 16px));
+}
+
+#first-time .tooltip-locate:before {
+ content: "";
+ position: absolute;
+ /*
+ * Arrow pointing near the locate icon.
+ */
+ left: 20px;
+ bottom: -10px;
+
+ width: 0;
+ height: 0;
+
+ border-left: 10px solid transparent;
+ border-right: 10px solid transparent;
+ border-top: 10px solid #fff;
+}
+#first-time button {
+ border: 0;
+ -webkit-border-radius: 0px;
+ -moz-border-radius: 0px;
+ border-radius: 0;
+ margin-bottom: 1.5em;
+}
+
+.number-icon {
+ background-image: url("marker-icon.png");
+ text-align:center;
+ color:White;
+ font-size: 20px;
+}
+
+@media screen and (max-width: 640px) {
+
+ #first-time .tooltip {
+ max-width: 90%;
+ right: 5%;
+ }
+ /*
+ test
+ */
+ /* #first-time .tooltip-locate {
+ max-width: 90%;
+ right: 5%;
+ /* top: 70%;
+ }
+
+ #first-time .tooltip-locate::before {
+ top: 124px;
+ }*/
+ #first-time .tooltip-locate {
+ width: auto;
+ max-width: calc(100vw - 32px);
+ }
+
+ #first-time .tooltip-locate::before {
+ top: auto;
+ left: 20px;
+ bottom: -10px;
+ transform: none;
+ }
+
+ #map {
+ height: 40em !important;
+ }
+}
+
+@media screen and (min-width: 60em) {
+ #filters {
+ height: auto;
+ }
+
+ .map #map {
+ min-height: 30em;
+ }
+}
diff --git a/views/public/fonts/icomoon.eot b/views/public/fonts/icomoon.eot
new file mode 100644
index 0000000..3a0a0bf
Binary files /dev/null and b/views/public/fonts/icomoon.eot differ
diff --git a/views/public/fonts/icomoon.svg b/views/public/fonts/icomoon.svg
new file mode 100644
index 0000000..9dab61d
--- /dev/null
+++ b/views/public/fonts/icomoon.svg
@@ -0,0 +1,33 @@
+
+
+
+Generated by IcoMoon
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/public/fonts/icomoon.ttf b/views/public/fonts/icomoon.ttf
new file mode 100644
index 0000000..bb1e4b0
Binary files /dev/null and b/views/public/fonts/icomoon.ttf differ
diff --git a/views/public/fonts/icomoon.woff b/views/public/fonts/icomoon.woff
new file mode 100644
index 0000000..0c4ca5b
Binary files /dev/null and b/views/public/fonts/icomoon.woff differ
diff --git a/views/public/fonts/icomoon/Read Me.txt b/views/public/fonts/icomoon/Read Me.txt
new file mode 100644
index 0000000..9d2b9e5
--- /dev/null
+++ b/views/public/fonts/icomoon/Read Me.txt
@@ -0,0 +1,3 @@
+To modify your generated font, use the *dev.svg* file, located in the *fonts* folder in this package. You can import this dev.svg file to the IcoMoon app. All the tags (class names) and the Unicode points of your glyphs are saved in this file.
+
+See the documentation for more info on how to use this package: http://icomoon.io/#docs/font-face
\ No newline at end of file
diff --git a/views/public/fonts/icomoon/fonts/icomoon.dev.svg b/views/public/fonts/icomoon/fonts/icomoon.dev.svg
new file mode 100644
index 0000000..776ccec
--- /dev/null
+++ b/views/public/fonts/icomoon/fonts/icomoon.dev.svg
@@ -0,0 +1,92 @@
+
+
+
+
+This is a custom SVG font generated by IcoMoon.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/public/fonts/icomoon/fonts/icomoon.eot b/views/public/fonts/icomoon/fonts/icomoon.eot
new file mode 100644
index 0000000..001fae3
Binary files /dev/null and b/views/public/fonts/icomoon/fonts/icomoon.eot differ
diff --git a/views/public/fonts/icomoon/fonts/icomoon.svg b/views/public/fonts/icomoon/fonts/icomoon.svg
new file mode 100644
index 0000000..c75c6ae
--- /dev/null
+++ b/views/public/fonts/icomoon/fonts/icomoon.svg
@@ -0,0 +1,92 @@
+
+
+
+
+This is a custom SVG font generated by IcoMoon.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/public/fonts/icomoon/fonts/icomoon.ttf b/views/public/fonts/icomoon/fonts/icomoon.ttf
new file mode 100644
index 0000000..4b5e97b
Binary files /dev/null and b/views/public/fonts/icomoon/fonts/icomoon.ttf differ
diff --git a/views/public/fonts/icomoon/fonts/icomoon.woff b/views/public/fonts/icomoon/fonts/icomoon.woff
new file mode 100644
index 0000000..72e911a
Binary files /dev/null and b/views/public/fonts/icomoon/fonts/icomoon.woff differ
diff --git a/views/public/fonts/icomoon/index.html b/views/public/fonts/icomoon/index.html
new file mode 100644
index 0000000..13719d1
--- /dev/null
+++ b/views/public/fonts/icomoon/index.html
@@ -0,0 +1,287 @@
+
+
+
+Your Font/Glyphs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ icon-search
+
+
+
+ icon-facebook
+
+
+
+ icon-twitter
+
+
+
+ icon-export
+
+
+
+ icon-house
+
+
+
+ icon-list
+
+
+
+ icon-arrow-left
+
+
+
+ icon-arrow-down
+
+
+
+ icon-arrow-up
+
+
+
+ icon-arrow-right
+
+
+
+ icon-pinterest
+
+
+
+ icon-googleplus
+
+
+
+ icon-rss
+
+
+
+ icon-reply
+
+
+
+ icon-user
+
+
+
+ icon-map
+
+
+
+ icon-cycle
+
+
+
+ icon-reload
+
+
+
+ icon-location
+
+
+
+ icon-equalizer
+
+
+
+ icon-tumblr
+
+
+
+ icon-cross
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/public/fonts/icomoon/license.txt b/views/public/fonts/icomoon/license.txt
new file mode 100644
index 0000000..22207a0
--- /dev/null
+++ b/views/public/fonts/icomoon/license.txt
@@ -0,0 +1,10 @@
+Icon Set: Entypo -- http://www.entypo.com/
+License: CC BY-SA 3.0 -- http://creativecommons.org/licenses/by-sa/3.0/
+
+
+Icon Set: IcoMoon - Free -- http://keyamoon.com/icomoon/
+License: CC BY 3.0 -- http://creativecommons.org/licenses/by/3.0/
+
+
+Icon Set: Iconic -- http://somerandomdude.com/work/iconic/
+License: CC BY-SA 3.0 -- http://creativecommons.org/licenses/by-sa/3.0/us/
\ No newline at end of file
diff --git a/views/public/fonts/icomoon/lte-ie7.js b/views/public/fonts/icomoon/lte-ie7.js
new file mode 100644
index 0000000..9ff2d47
--- /dev/null
+++ b/views/public/fonts/icomoon/lte-ie7.js
@@ -0,0 +1,49 @@
+/* Load this script using conditional IE comments if you need to support IE 7 and IE 6. */
+
+window.onload = function() {
+ function addIcon(el, entity) {
+ var html = el.innerHTML;
+ el.innerHTML = '' + entity + ' ' + html;
+ }
+ var icons = {
+ 'icon-search' : '',
+ 'icon-facebook' : '',
+ 'icon-twitter' : '',
+ 'icon-export' : '',
+ 'icon-house' : '',
+ 'icon-list' : '',
+ 'icon-arrow-left' : '',
+ 'icon-arrow-down' : '',
+ 'icon-arrow-up' : '',
+ 'icon-arrow-right' : '',
+ 'icon-pinterest' : '',
+ 'icon-googleplus' : '',
+ 'icon-rss' : '',
+ 'icon-reply' : '',
+ 'icon-user' : '',
+ 'icon-map' : '',
+ 'icon-cycle' : '',
+ 'icon-reload' : '',
+ 'icon-location' : '',
+ 'icon-equalizer' : '',
+ 'icon-tumblr' : '',
+ 'icon-cross' : ''
+ },
+ els = document.getElementsByTagName('*'),
+ i, attr, c, el;
+ for (i = 0; ; i += 1) {
+ el = els[i];
+ if(!el) {
+ break;
+ }
+ attr = el.getAttribute('data-icon');
+ if (attr) {
+ addIcon(el, attr);
+ }
+ c = el.className;
+ c = c.match(/icon-[^\s'"]+/);
+ if (c && icons[c[0]]) {
+ addIcon(el, icons[c[0]]);
+ }
+ }
+};
\ No newline at end of file
diff --git a/views/public/fonts/icomoon/style.css b/views/public/fonts/icomoon/style.css
new file mode 100644
index 0000000..d0061a6
--- /dev/null
+++ b/views/public/fonts/icomoon/style.css
@@ -0,0 +1,106 @@
+@font-face {
+ font-family: 'icomoon';
+ src:url('fonts/icomoon.eot');
+ src:url('fonts/icomoon.eot?#iefix') format('embedded-opentype'),
+ url('fonts/icomoon.woff') format('woff'),
+ url('fonts/icomoon.ttf') format('truetype'),
+ url('fonts/icomoon.svg#icomoon') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+
+/* Use the following CSS code if you want to use data attributes for inserting your icons */
+[data-icon]:before {
+ font-family: 'icomoon';
+ content: attr(data-icon);
+ speak: none;
+ font-weight: normal;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* Use the following CSS code if you want to have a class per icon */
+/*
+Instead of a list of all class selectors,
+you can use the generic selector below, but it's slower:
+[class*="icon-"] {
+*/
+.icon-search, .icon-facebook, .icon-twitter, .icon-export, .icon-house, .icon-list, .icon-arrow-left, .icon-arrow-down, .icon-arrow-up, .icon-arrow-right, .icon-pinterest, .icon-googleplus, .icon-rss, .icon-reply, .icon-user, .icon-map, .icon-cycle, .icon-reload, .icon-location, .icon-equalizer, .icon-tumblr, .icon-cross {
+ font-family: 'icomoon';
+ speak: none;
+ font-style: normal;
+ font-weight: normal;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+}
+.icon-search:before {
+ content: "\e000";
+}
+.icon-facebook:before {
+ content: "\e002";
+}
+.icon-twitter:before {
+ content: "\e001";
+}
+.icon-export:before {
+ content: "\e003";
+}
+.icon-house:before {
+ content: "\e004";
+}
+.icon-list:before {
+ content: "\e005";
+}
+.icon-arrow-left:before {
+ content: "\e006";
+}
+.icon-arrow-down:before {
+ content: "\e007";
+}
+.icon-arrow-up:before {
+ content: "\e008";
+}
+.icon-arrow-right:before {
+ content: "\e009";
+}
+.icon-pinterest:before {
+ content: "\e00a";
+}
+.icon-googleplus:before {
+ content: "\e00b";
+}
+.icon-rss:before {
+ content: "\e00c";
+}
+.icon-reply:before {
+ content: "\e00d";
+}
+.icon-user:before {
+ content: "\e00e";
+}
+.icon-map:before {
+ content: "\e00f";
+}
+.icon-cycle:before {
+ content: "\e010";
+}
+.icon-reload:before {
+ content: "\e011";
+}
+.icon-location:before {
+ content: "\e012";
+}
+.icon-equalizer:before {
+ content: "\e013";
+}
+.icon-tumblr:before {
+ content: "\e014";
+}
+.icon-cross:before {
+ content: "\e600";
+}
diff --git a/views/public/images/bg.jpg b/views/public/images/bg.jpg
old mode 100644
new mode 100755
diff --git a/views/public/images/location.png b/views/public/images/location.png
old mode 100644
new mode 100755
diff --git a/views/public/index/index.php b/views/public/index/index.php
old mode 100644
new mode 100755
index 5655e58..4bb7c51
--- a/views/public/index/index.php
+++ b/views/public/index/index.php
@@ -1,61 +1,59 @@
-
-
'map')); ?>
- Home ', array('id' => 'home-button')); ?>
-
-
+Home', array('id' => 'home-button')); ?>
+
+
+
-
Map On
+
Map
+ On
Filters
-
-
Select Filters
-
Map Era
-
- All Map Eras
- map_coverages as $map_coverage): ?>
-
-
-
-
Item Type
-
- All Item Types
- item_types as $item_type_id => $item_type): ?>
-
-
-
-
-
+
+
-
+
\ No newline at end of file
diff --git a/views/public/javascripts/mall-map.js b/views/public/javascripts/mall-map.js
deleted file mode 100644
index 959377b..0000000
--- a/views/public/javascripts/mall-map.js
+++ /dev/null
@@ -1,446 +0,0 @@
-$(document).ready(function () {
-
- // Set map height to be window height minus header height.
- var windowheight = $(window).height();
- $('#map').css('height', windowheight - 54);
-
-
- var MAP_URL_TEMPLATE = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
- var MAP_CENTER = [38.8891, -77.02949];
- var MAP_ZOOM = 15;
- var MAP_MIN_ZOOM = 14;
- var MAP_MAX_ZOOM = 18;
- var MAP_MAX_BOUNDS = [[38.79164, -77.17232], [38.99583, -76.90917]];
- var LOCATE_BOUNDS = [[38.87814, -77.05656], [38.90025, -77.00678]];
- var MAX_LOCATE_METERS = 8000;
-
- var map;
- var historicMapLayer;
- var markers;
- var jqXhr;
- var locationMarker;
-
- // Set the base map layer.
- map = L.map('map', {
- center: MAP_CENTER,
- zoom: MAP_ZOOM,
- minZoom: MAP_MIN_ZOOM,
- maxZoom: MAP_MAX_ZOOM,
- maxBounds: MAP_MAX_BOUNDS,
- zoomControl: false
- });
- map.addLayer(L.tileLayer(MAP_URL_TEMPLATE));
- map.addControl(L.control.zoom({position: 'topleft'}));
- map.attributionControl.setPrefix('');
-
- // Check for user's first time visiting. Wait to locate the user after displaying tooltip on the first visit.
- if(!($.cookie('myCookie'))) {
- $('#first-time').show();
- $.cookie('myCookie','visited', { path: '/', expires: 10000 });
- } else {
- map.locate({watch: true});
- }
-
- $("#first-time button").on('click', function() {
- $('#first-time').hide();
- map.locate({watch: true});
- });
-
- // Retain previous form state, if needed.
- retainFormState();
-
- // Add all markers by default, or retain previous marker state.
- doFilters();
-
- // Handle location found.
- map.on('locationfound', function (e) {
- // User within location bounds. Set the location marker.
- if (L.latLngBounds(LOCATE_BOUNDS).contains(e.latlng)) {
- launchTooltip();
- if (locationMarker) {
- // Remove the existing location marker before adding to map.
- map.removeLayer(locationMarker);
- } else {
- // Pan to location only on first locate.
- map.panTo(e.latlng);
- }
- $('#locate-button').removeClass('disabled');
- locationMarker = L.marker(e.latlng, {
- icon: L.icon({
- iconUrl: 'plugins/MallMap/views/public/images/location.png',
- iconSize: [50, 50]
- })
- });
- locationMarker.addTo(map).
- bindPopup("You are within " + e.accuracy / 2 + " meters from this point");
- // User outside location bounds.
- } else {
- map.stopLocate();
- $('#locate-button').addClass('disabled');
- var locateMeters = e.latlng.distanceTo(map.options.center);
- // Show out of bounds message only if within a certain distance.
- if (MAX_LOCATE_METERS > locateMeters) {
- var locateMiles = Math.ceil((locateMeters * 0.000621371) * 100) / 100;
- $('#dialog').text('You are ' + locateMiles + ' miles from the National Mall.').
- dialog('option', 'title', 'Not Quite on the Mall').
- dialog('open');
- }
-
- }
- });
-
- // Handle location error.
- map.on('locationerror', function () {
- map.stopLocate();
- $('#locate-button').addClass('disabled');
- });
-
- // Set up the dialog window.
- $('#dialog').dialog({
- autoOpen: false,
- draggable: false,
- resizable: false
- });
-
- // Handle the filter form.
- $('#filter-button').click(function(e) {
- e.preventDefault();
- var filterButton = $(this);
- var clicks = filterButton.data('clicks');
- if (clicks) {
- filterButton.removeClass('on').
- find('.screen-reader-text').
- html('Filters');
- $('#filters').fadeToggle(200, 'linear');
- } else {
- filterButton.addClass('on').
- find('.screen-reader-text').
- html('Back to Map');
- $('#filters').fadeToggle(200, 'linear');
- }
- filterButton.data('clicks', !clicks);
- });
-
- // Revert form to default and display all markers.
- $('#all-button').click(function(e) {
- e.preventDefault();
- revertFormState();
- });
-
- // Handle locate button.
- $('#locate-button').click(function (e) {
- e.preventDefault();
- if ($(this).hasClass('disabled')) {
- return;
- }
- if (locationMarker) {
- map.removeLayer(locationMarker)
- locationMarker = null;
- }
- map.stopLocate();
- map.locate({watch: true});
- });
-
- // Toggle historic map layer on and off.
- $('#toggle-map-button').click(function (e) {
- e.preventDefault();
- var toggleMapButton = $(this);
- var clicks = toggleMapButton.data('clicks');
- if (clicks) {
- toggleMapButton.addClass('on');
- toggleMapButton.find('.screen-reader-text').html('Map On');
- map.addLayer(historicMapLayer);
- } else {
- if (historicMapLayer) {
- toggleMapButton.removeClass('on');
- toggleMapButton.find('.screen-reader-text').html('Map Off');
- map.removeLayer(historicMapLayer);
- }
- }
- toggleMapButton.data('clicks', !clicks);
- });
-
- // Toggle map filters
- $('#filters div label').click(function() {
- var checkboxLabel = $(this);
- if (checkboxLabel.find('input[type=checkbox]').is(':checked')) {
- checkboxLabel.addClass('on');
- } else {
- checkboxLabel.removeClass('on');
- }
- });
-
- // Filter historic map layer.
- $('#map-coverage').change(function () {
- if (historicMapLayer) {
- removeHistoricMapLayer();
- }
- if ('0' == $('#map-coverage').val()) {
- $('#toggle-map-button').hide();
- } else {
- addHistoricMapLayer();
- }
- doFilters();
- });
-
- // Filter item type.
- $('#item-type').change(function () {
- var itemType = $(this);
- if ('Place' == itemType.find(':selected').text()) {
- $('#place-type-div').show({duration: 'fast'});
- } else {
- // Reset and hide the place type select.
- $('input[name=place-type]').removeAttr('checked');
- $('#place-type-div').hide({duration: 'fast'});
- }
- if ('Event' == itemType.find(':selected').text()) {
- $('#event-type-div').show({duration: 'fast'});
- } else {
- // Reset and hide the event type checkboxes.
- $('input[name=event-type]').removeAttr('checked');
- $('#event-type-div').hide({duration: 'fast'});
- }
- doFilters();
- });
-
- $('#map-coverage,#item-type').on('touchstart touchend', function(event) {
- event.stopPropagation();
- });
-
- // Filter place type.
- $('input[name=place-type]').change(function () {
- // Handle all place types checkbox.
- var placeTypeAll = $('input[name=place-type-all]');
- if ($('input[name=place-type]:checked').length) {
- placeTypeAll.prop('checked', false).parent().removeClass('on');
- } else {
- placeTypeAll.prop('checked', true).parent().addClass('on');
- }
- doFilters();
- });
-
- // Handle the all place types checkbox.
- $('input[name=place-type-all]').change(function () {
- // Uncheck all place types.
- $('input[name=place-type]:checked').prop('checked', false).
- parent().removeClass('on');
- doFilters();
- });
-
- // Filter event type.
- $('input[name=event-type]').change(function () {
- // Handle all event types checkbox.
- var eventTypeAll = $('input[name=event-type-all]');
- if ($('input[name=event-type]:checked').length) {
- eventTypeAll.prop('checked', false).parent().removeClass('on');
- } else {
- eventTypeAll.prop('checked', true).parent().addClass('on');
- }
- doFilters();
- });
-
- // Handle the all event types checkbox.
- $('input[name=event-type-all]').change(function () {
- // Uncheck all event types.
- $('input[name=event-type]:checked').prop('checked', false).
- parent().removeClass('on');
- doFilters();
- });
-
- // Handle the info panel back button.
- $('a.back-button').click(function (e) {
- e.preventDefault();
- $('#info-panel').fadeToggle(200, 'linear');
- $('#toggle-map-button + .back-button').hide();
- });
-
- /*
- * Filter markers.
- *
- * This must be called on every form change.
- */
- function doFilters() {
- // Prevent concurrent filter requests.
- if (jqXhr) {
- jqXhr.abort()
- }
-
- // Remove the current markers.
- if (markers) {
- map.removeLayer(markers);
- }
-
- var mapCoverage = $('#map-coverage');
- var itemType = $('#item-type');
- var placeTypes = $('input[name=place-type]:checked');
- var eventTypes = $('input[name=event-type]:checked');
-
- // Prepare POST data object for request.
- var postData = {
- placeTypes: [],
- eventTypes: [],
- };
-
- // Handle each filter, adding to the POST data object.
- if ('0' != mapCoverage.val()) {
- postData['mapCoverage'] = mapCoverage.val();
- }
- if ('0' != itemType.val()) {
- postData['itemType'] = itemType.val();
- }
- if (placeTypes.length) {
- placeTypes.each(function () {
- postData.placeTypes.push(this.value);
- });
- }
- if (eventTypes.length) {
- eventTypes.each(function () {
- postData.eventTypes.push(this.value);
- });
- }
-
- // Make the POST request, handle the GeoJSON response, and add markers.
- jqXhr = $.post('mall-map/index/filter', postData, function (response) {
- var item = (1 == response.features.length) ? 'item' : 'items';
- $('#marker-count').text(response.features.length + " " + item);
- var geoJsonLayer = L.geoJson(response, {
- onEachFeature: function (feature, layer) {
- layer.on('click', function (e) {
- // Request the item data and populate and open the marker popup.
- var marker = this;
- $.post('mall-map/index/get-item', {id: feature.properties.id}, function (response) {
- var popupContent = '
' + response.title + ' ';
- if (response.thumbnail) {
- popupContent += '
' + response.thumbnail + ' ';
- }
- popupContent += '
view more info ';
- marker.bindPopup(popupContent, {maxWidth: 200, offset: L.point(0, -40)}).openPopup();
-
- window.setTimeout(function () {
- //map.panTo([feature.geometry.coordinates[1],feature.geometry.coordinates[0]]);
- layer.getPopup().update();
- $('.open-info-panel').click(function (e) {
- e.preventDefault();
- $('#info-panel').fadeToggle(200, 'linear');
- $('#toggle-map-button + .back-button').show();
- });
- }, 500);
-
- // Populate the item info panel.
- var content = $('#info-panel-content');
- content.empty();
- content.append('
' + response.title + ' ');
- for (var i = 0; i < response.date.length; i++) {
- content.append('
' + response.date[i] + '
');
- }
- content.append('
' + response.description + '
');
- content.append(response.fullsize);
- content.append('
view more info
');
- });
- });
- }
- });
- markers = new L.MarkerClusterGroup({
- showCoverageOnHover: false,
- maxClusterRadius: 40,
- spiderfyDistanceMultiplier: 2
- });
- markers.addLayer(geoJsonLayer);
- map.addLayer(markers);
- });
- }
-
- /*
- * Add the historic map layer.
- */
- function addHistoricMapLayer()
- {
- // Get the historic map data
- var getData = {'text': $('#map-coverage').val()};
- $.get('mall-map/index/historic-map-data', getData, function (response) {
- historicMapLayer = L.tileLayer(
- response.url,
- {tms: true, opacity: 1.00}
- );
- map.addLayer(historicMapLayer);
- $('#toggle-map-button').show();
-
- // Set the map title as the map attribution prefix.
- map.attributionControl.setPrefix(response.title);
- });
- }
-
- /*
- * Remove the historic map layer.
- */
- function removeHistoricMapLayer()
- {
- $('#toggle-map-button').data('clicks', false).hide();
- map.removeLayer(historicMapLayer);
- map.attributionControl.setPrefix('');
- }
-
- /*
- * Revert to default (original) form state.
- */
- function revertFormState()
- {
- if (historicMapLayer) {
- removeHistoricMapLayer();
- }
-
- $('#map-coverage').val('0');
- $('#item-type').val('0');
-
- $('#place-type-div').hide({duration: 'fast'});
- $('input[name=place-type-all]').prop('checked', true).
- parent().addClass('on');
- $('input[name=place-type]:checked').prop('checked', false).
- parent().removeClass('on');
-
- $('#event-type-div').hide({duration: 'fast'});
- $('input[name=event-type-all]').prop('checked', true).
- parent().addClass('on');
- $('input[name=event-type]:checked').prop('checked', false).
- parent().removeClass('on');
-
- doFilters();
- }
-
- /*
- * Retain previous form state.
- *
- * Acts on the assumption that all browsers will preserve the form state
- * when navigating back to the map from another page.
- */
- function retainFormState()
- {
- if ('0' != $('#map-coverage').val()) {
- addHistoricMapLayer();
- }
- if ('Place' == $('#item-type').find(':selected').text()) {
- var placeTypes = $('input[name=place-type]:checked');
- if (placeTypes.length) {
- $('input[name=place-type-all]').parent().removeClass('on');
- placeTypes.parent().addClass('on');
- }
- $('#place-type-div').show({duration: 'fast'});
- }
- if ('Event' == $('#item-type').find(':selected').text()) {
- var eventTypes = $('input[name=event-type]:checked');
- if (eventTypes.length) {
- $('input[name=event-type-all]').parent().removeClass('on');
- eventTypes.parent().addClass('on');
- }
- $('#event-type-div').show({duration: 'fast'});
- }
- }
-
- var debugTimestamp;
- function start() {
- debugTimestamp = new Date().getTime();
- }
- function stop() {
- console.log((new Date().getTime() / 1000) - (debugTimestamp / 1000));
- }
-});
diff --git a/views/public/javascripts/walking-tour.js b/views/public/javascripts/walking-tour.js
new file mode 100755
index 0000000..ee15ef5
--- /dev/null
+++ b/views/public/javascripts/walking-tour.js
@@ -0,0 +1,923 @@
+$(document).ready(function () {
+ walkingTourJs()
+});
+
+function walkingTourJs() {
+ var imported = document.createElement("script");
+ document.head.appendChild(imported);
+ // Set map height to be window height minus header height.
+ var windowheight = $(window).height();
+ $('#map').css('height', windowheight - 54);
+
+ var MAP_URL_TEMPLATE = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}';
+
+ var MAP_CENTER;
+ var MAP_ZOOM; // MAP_ZOOM controls the default zoom of the map
+ var MAP_MIN_ZOOM_STOP;
+ var MAP_MAX_ZOOM_STOP;
+ var LOCATE_BOUNDS;
+ var EXHIBIT_BUTTON_TEXT;
+ var DETAIL_BUTTON_TEXT;
+ var IS_AUTO_FIT;
+
+ var map;
+ var historicMapLayer;
+ var markers;
+ var jqXhr;
+ var locationMarker;
+ var markerData;
+ var allItems = {};
+ var allMarkers = {};
+ function positionLocateTooltip() {
+ var tooltip = document.querySelector('#first-time .tooltip-locate');
+ var target = document.querySelector('#locate-button');
+
+ if (!tooltip || !target) {
+ return;
+ }
+
+ var rect = target.getBoundingClientRect();
+
+ /*
+ * Align tooltip's left edge near the locate button.
+ * The locate button is close to the left edge, so we do not center
+ * the tooltip horizontally; otherwise it can go off screen.
+ */
+ tooltip.style.left = rect.left + 'px';
+ tooltip.style.top = rect.top + 'px';
+ }
+
+
+ /*
+ * JQuery Setup
+ */
+
+ // Check for user's first time visiting. Wait to locate the user after displaying tooltip on the first visit.
+ if (!($.cookie('myCookie'))) {
+ $('#first-time').show();
+ // $('.tooltip-locate').toggle();
+ $('.tooltip-locate').hide();
+ $.cookie('myCookie', 'visited', { path: '/', expires: 10000 });
+ }
+
+ $("#first-time > div.tooltip > button").on('click', function () {
+ $('.tooltip').fadeToggle();
+ positionLocateTooltip();
+ $('.tooltip-locate').fadeToggle();
+ });
+
+ $("#first-time > div.tooltip-locate > button").on('click', function () {
+ $('#first-time').hide();
+ });
+
+ $(window).on('resize', function () {
+ positionLocateTooltip();
+ });
+
+ // Set up the dialog window.
+ $('#dialog').dialog({
+ autoOpen: false,
+ draggable: false,
+ resizable: false
+ });
+
+ // Handle the filter form.
+ $('#filter-button').click(function (e) {
+ e.preventDefault();
+
+ // First close any popups
+ var popupDiv = $('.leaflet-pane .leaflet-popup-pane')
+ if (popupDiv.children().length > 0) {
+ popupDiv.empty()
+ }
+
+ var filterButton = $(this);
+ var clicks = filterButton.data('clicks');
+ if (clicks) {
+ filterButton.removeClass('on').
+ find('.screen-reader-text').
+ html('Filters');
+ $('#filters').fadeToggle(200, 'linear');
+ } else {
+ filterButton.addClass('on').
+ find('.screen-reader-text').
+ html('Back to Map');
+ $('#filters').fadeToggle(200, 'linear');
+ }
+ filterButton.data('clicks', !clicks);
+ });
+
+ $('#tour-confirm-button').click(function (e) {
+ e.preventDefault();
+ var filterButton = $('#filter-button');
+ var tourSelected = []
+ var tourTypeCheck = $('input[name=place-type]:checked')
+ var curTourSelected;
+ var itemIDList = [];
+ var tour_id;
+
+ if (tourTypeCheck.length) {
+ tourTypeCheck.each(function () {
+ tour_id = this.value;
+ tourSelected.push(markerData[this.value].walkingPath);
+ curTourSelected = markerData[this.value];
+ });
+ } else {
+ var toursToPlot = Object.keys(markerData);
+ toursToPlot.forEach((ele) => {
+ tourSelected.push(markerData[ele].walkingPath);
+ });
+ }
+
+ filterButton.removeClass('on').
+ find('.screen-reader-text').
+ html('Filters');
+ $('#filters').fadeToggle(200, 'linear');
+
+ if (curTourSelected) {
+ curTourSelected.Data.features.forEach(ele => {
+ itemIDList.push(ele.properties.id)
+ })
+
+ $('#info-panel-container').fadeToggle(200, 'linear');
+ $('#toggle-map-button + .back-button').show();
+ populateTourIntroPopup(itemIDList, curTourSelected, tour_id);
+ }
+ let polylineGroup = L.featureGroup(tourSelected);
+ let bounds = polylineGroup.getBounds();
+ map.fitBounds(bounds);
+ })
+
+ // Revert form to default and display all markers.
+ $('#all-button').click(function (e) {
+ e.preventDefault();
+ revertFormState();
+ });
+
+ // Handle locate button.
+ $('#locate-button').click(function (e) {
+ e.preventDefault();
+ $(this).toggleClass('loading');
+ if (locationMarker) {
+ map.removeLayer(locationMarker)
+ locationMarker = null;
+ }
+ map.stopLocate();
+ map.locate({ watch: true });
+ });
+
+ // Toggle historic map layer on and off.
+ $('#toggle-map-button').click(function (e) {
+ e.preventDefault();
+ var toggleMapButton = $(this);
+ var clicks = toggleMapButton.data('clicks');
+ if (clicks) {
+ toggleMapButton.addClass('on');
+ toggleMapButton.find('.screen-reader-text').html('Map On');
+ map.addLayer(historicMapLayer);
+ } else {
+ if (historicMapLayer) {
+ toggleMapButton.removeClass('on');
+ toggleMapButton.find('.screen-reader-text').html('Map Off');
+ map.removeLayer(historicMapLayer);
+ }
+ }
+ toggleMapButton.data('clicks', !clicks);
+ });
+
+ // Toggle map filters
+ $('#filters div label').click(function () {
+ var checkboxLabel = $(this);
+ if (checkboxLabel.find('input[type=checkbox]').is(':checked')) {
+ checkboxLabel.addClass('on');
+ } else {
+ checkboxLabel.removeClass('on');
+ }
+ });
+
+ // Filter historic map layer.
+ $('#map-coverage').change(function () {
+ if (historicMapLayer) {
+ removeHistoricMapLayer();
+ }
+ if ('0' == $('#map-coverage').val()) {
+ $('#toggle-map-button').hide();
+ } else {
+ addHistoricMapLayer();
+ }
+ doFilters();
+ });
+
+ $('#map-coverage,#tour-type').on('touchstart touchend', function (event) {
+ event.stopPropagation();
+ });
+
+ // Filter place type.
+ $('input[name=place-type]').change(function () {
+ // allow only one box checked
+ $('input[name=place-type]:checked').not(this).prop('checked', false).
+ parent().removeClass('on');
+ // Handle all place types checkbox.
+ var placeTypeAll = $('input[name=place-type-all]');
+ if ($('input[name=place-type]:checked').length) {
+ placeTypeAll.prop('checked', false).parent().removeClass('on');
+ } else {
+ placeTypeAll.prop('checked', true).parent().addClass('on');
+ }
+ doFilters();
+ });
+
+ // Handle the all place types checkbox.
+ $('input[name=place-type-all]').change(function () {
+ // Uncheck all place types.
+ $('input[name=place-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+ doFilters();
+ });
+
+ // Filter event type.
+ $('input[name=event-type]').change(function () {
+ // Handle all event types checkbox.
+ var eventTypeAll = $('input[name=event-type-all]');
+ if ($('input[name=event-type]:checked').length) {
+ eventTypeAll.prop('checked', false).parent().removeClass('on');
+ } else {
+ eventTypeAll.prop('checked', true).parent().addClass('on');
+ }
+ doFilters();
+ });
+
+ // Handle the all event types checkbox.
+ $('input[name=event-type-all]').change(function () {
+ // Uncheck all event types.
+ $('input[name=event-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+ doFilters();
+ });
+
+ // Handle the info panel back button.
+ $('a.back-button').click(function (e) {
+ e.preventDefault();
+ $('#info-panel-container').fadeToggle(200, 'linear');
+ });
+
+ /*
+ * Query backend
+ */
+
+ window.onload = function () {
+ jqXhr = $.post('walking-tour/index/map-config', function (response) {
+ mapSetUp(response);
+ doQuery();
+ })
+ };
+
+ // Retain previous form state, if needed.
+ retainFormState();
+
+ function mapLocateCenter(map){
+ map.flyTo(MAP_CENTER, MAP_ZOOM);
+ }
+
+ /*
+ * Setup map layer
+ *
+ * Call only once during set up
+ */
+ function mapSetUp(response) {
+ EXHIBIT_BUTTON_TEXT = response['walking_tour_exhibit_button']
+ DETAIL_BUTTON_TEXT = response['walking_tour_detail_button']
+ MAP_ZOOM = parseInt(response["walking_tour_default_zoom"])
+ MAP_MAX_ZOOM_STOP = parseInt(response['walking_tour_max_zoom'])
+ MAP_MIN_ZOOM_STOP = parseInt(response['walking_tour_min_zoom'])
+ MAP_CENTER = parse1DArrayPoint("[" + response['walking_tour_center'] + ']')
+ IS_AUTO_FIT = response["walking_tour_auto_fit"] == '1'
+ // Set the base map layer.
+ var minZoom = MAP_ZOOM - MAP_MIN_ZOOM_STOP;
+ var maxZoom = MAP_ZOOM + MAP_MAX_ZOOM_STOP;
+ map = L.map('map', {
+ center: MAP_CENTER,
+ minZoom: minZoom,
+ maxZoom: maxZoom,
+ zoom: minZoom,
+ zoomControl: false
+ });
+ LOCATE_BOUNDS = map.getBounds();
+ map.setZoom(MAP_ZOOM);
+
+ map.addLayer(L.tileLayer(MAP_URL_TEMPLATE));
+ map.addControl(L.control.zoom({ position: 'topleft' }));
+ var extentControl = L.Control.extend({
+ options: {
+ position: 'topleft'
+ },
+ onAdd: function (map) {
+ var container = L.DomUtil.create('div', 'extentControl');
+ $(container).attr('id', 'extent-control');
+ $(container).css('width', '26px').css('height', '26px').css('outline', '1px black');
+ $(container).addClass('extentControl-disabled')
+ $(container).addClass('leaflet-bar')
+ $(container).on('click', function () {
+ mapLocateCenter(map);
+ });
+ return container;
+ }
+ })
+ map.addControl(new extentControl());
+ map.attributionControl.setPrefix('Tiles © Esri');
+
+ map.on('zoomend', function () {
+ if (map.getZoom() == minZoom) {
+ $('#extent-control').addClass('extentControl-disabled')
+ } else {
+ $('#extent-control').removeClass('extentControl-disabled')
+ }
+ })
+
+ // Handle location found.
+ map.on('locationfound', function (e) {
+ if (!locationMarker) {
+ $("#locate-button").toggleClass('loading');
+ }
+ // User within location bounds. Set the location marker.
+ if (L.latLngBounds(LOCATE_BOUNDS).contains(e.latlng)) {
+ if (locationMarker) {
+ // Remove the existing location marker before adding to map.
+ map.removeLayer(locationMarker);
+ } else {
+ // Pan to location only on first locate.
+ map.panTo(e.latlng);
+ }
+ locationMarker = L.marker(e.latlng, {
+ icon: L.icon({
+ iconUrl: 'plugins/WalkingTour/views/public/images/location.png',
+ iconSize: [25, 25]
+ })
+ });
+ locationMarker.addTo(map).bindPopup("You are within " + e.accuracy / 2 + " meters from this point");
+ // User outside location bounds.
+ } else {
+ var locateMeters = e.latlng.distanceTo(map.options.center);
+ var locateMiles = Math.ceil((locateMeters * 0.000621371) * 100) / 100;
+ alert('Cannot locate your location. You are ' + locateMiles + ' miles from the map bounds.');
+ map.stopLocate();
+ }
+ });
+
+ // Handle location error.
+ map.on('locationerror', function () {
+ $("#locate-button").toggleClass('loading');
+ map.stopLocate();
+ alert('Location Error, Please try again.');
+ console.log('location error')
+ });
+ }
+
+ /*
+ * Query backend for tour info
+ *
+ * Call only once during set up
+ */
+ function doQuery() {
+ const markerFontHtmlStyles = `
+ transform: rotate(-45deg);
+ color:white;
+ text-align: center;
+ padding: 0.2rem 0 0.18rem 0;
+ font-size: 15px;
+ `
+
+ // correctly formats coordinates as [lat, long] (API returns [long, lat])
+ function orderCoords(path) {
+ var directions = [];
+ for (var i = 0; i < path.length; i++) {
+ directions.push([path[i][1], path[i][0]]);
+ }
+ return directions;
+ }
+
+ var key = "5b3ce3597851110001cf62489dde4c6690bc423bb86bd99921c5da77";
+ var url;
+ var itemArray = []
+ var tourToItem = {}
+ var markerBounds = L.latLngBounds();
+ jqXhr = $.post('walking-tour/index/query', function (response) {
+ markerData = response;
+ dataArray = Object.entries(markerData)
+ for (const tour in markerData) {
+ itemArray = itemArray.concat(markerData[tour]['Data']['features'])
+ }
+ let requests = dataArray.map(([tourId, value]) => {
+ return new Promise((resolve) => {
+ var numMarker = 1;
+ var response = value["Data"];
+ var itemIDList = [];
+
+ response.features.forEach(ele => {
+ itemIDList.push(ele.properties.id)
+ })
+ tourToItem[tourId] = itemIDList;
+ markerList = []
+ var geoJsonLayer = L.geoJson(response.features, {
+ // adds the correct number to each marker based on order of tour
+ pointToLayer: function (feature, latlng) {
+ var numberIcon = L.divIcon({
+ className: "my-custom-pin",
+ iconSize: [25, 41],
+ iconAnchor: [12, 40],
+ popupAnchor: [0, -5],
+ html: `
${numMarker}
`
+ });
+ numMarker++;
+ markerBounds.extend(latlng);
+ return L.marker(latlng, { icon: numberIcon });
+ },
+ onEachFeature: function (feature, layer) {
+ layer.on('click', function (e) {
+ // center click location
+ map.flyTo(e.latlng,MAP_ZOOM + MAP_MAX_ZOOM_STOP);
+ // Close the filtering
+ var filterButton = $('filter-button');
+ filterButton.removeClass('on').
+ find('.screen-reader-text').
+ html('Filters');
+ $('#filters').fadeOut(200, 'linear');
+
+ var marker = this;
+ response = allItems[`${tourId}:${feature.properties.id}`]
+ if (response == undefined) {
+ $.post('walking-tour/index/get-item', { id: feature.properties.id, tour: tourId }, function (response) {
+ allItems[`${tourId}:${feature.properties.id}`] = response;
+ featureOnclickAction(response, layer, marker, itemIDList, value, tourId);
+ })
+ } else {
+ featureOnclickAction(response, layer, marker, itemIDList, value, tourId);
+ }
+
+ });
+
+ }
+ });
+ markerData[tourId].allMarker = markerList;
+ markerData[tourId].geoJson = geoJsonLayer;
+
+ const path = JSON.parse(value.Route);
+ const route = path.features[0].geometry.coordinates.map(coord => [coord[1], coord[0]]);;
+
+ var tourPolyline = new L.Polyline(route, {
+ color: value["Color"],
+ weight: 3,
+ opacity: 1,
+ smoothFactor: 1
+ });
+
+ markerData[tourId].walkingPath = tourPolyline;
+ resolve();
+ });
+ })
+ Promise.all(requests).then(() => {
+ createCustomCSS();
+ if (IS_AUTO_FIT){
+ map.fitBounds(markerBounds, {padding: [10, 10]})
+ mapLocateCenter = function(map) {
+ map.fitBounds(markerBounds, {padding: [10, 10]})
+ }
+ var curZoom = map._zoom;
+ map.setMaxZoom( curZoom + MAP_MAX_ZOOM_STOP);
+ map.setMinZoom( curZoom - MAP_MIN_ZOOM_STOP);
+ // map["options"]["minZoom"] = curZoom - MAP_MIN_ZOOM_STOP
+ }
+ doFilters();
+ });
+ });
+ }
+
+ /*
+ * Filter markers.
+ *
+ * This must be called on every form change.
+ */
+ function doFilters() {
+ // Remove the current markers.
+ if (markers) {
+ map.removeLayer(markers);
+ }
+
+ var mapCoverage = $('#map-coverage');
+ var tourTypeCheck = $('input[name=place-type]:checked')
+
+ var toursToPlot = [];
+ var mapToPlot;
+ // Handle each filter
+ if ('0' != mapCoverage.val()) {
+ mapToPlot = mapCoverage.val();
+ }
+
+ if (tourTypeCheck.length) {
+ tourTypeCheck.each(function () {
+ toursToPlot.push(this.value);
+ });
+ } else {
+ toursToPlot = Object.keys(markerData);
+ }
+
+ var pathToPlot = [];
+ var markerLayers = [];
+ var numMarkers = 0;
+
+ // handle the GeoJSON response, and add markers.
+ toursToPlot.forEach(ele => {
+ numMarkers += markerData[ele].Data.features.length;
+ markerLayers.push(markerData[ele].geoJson);
+ pathToPlot.push(markerData[ele].walkingPath);
+ });
+ //response is an array of coordinate;
+ var item = (1 == numMarkers) ? 'item' : 'items';
+ $('#marker-count').text(numMarkers + " " + item);
+
+ try {
+ markers = new L.layerGroup();
+ }
+ catch (err) {
+ console.log(err)
+ }
+
+ markerLayers.forEach(ele => {
+ markers.addLayer(ele);
+ })
+ pathToPlot.forEach(ele => {
+ ele.addTo(markers);
+ })
+ map.addLayer(markers);
+ }
+
+ /*
+ * Setup color for each tour
+ *
+ */
+ function createCustomCSS() {
+ var style = document.createElement('style')
+ var css = ""
+ for (const tour_id in markerData) {
+ var color = markerData[tour_id]['Color']
+
+ if (color.length == 0){
+ color = "#000000"
+ }
+
+ var rgb = hexToRgb(color)
+ css += `#filters div label.label${tour_id}:before {
+ background-color: ${color} !important;
+ }
+ #filters div label.label${tour_id} {
+ background-color: rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.15) !important;
+ color: ${color} !important;
+ }
+ #filters div label.label${tour_id}.on:before {
+ background-color: rgba(33, 201, 0, 1) !important;
+ }\n`
+ }
+ style.innerHTML = css;
+ document.body.appendChild(style);
+ }
+
+ /*
+ * Setup popups for each item and tour
+ */
+
+ function featureOnclickAction(response, layer, marker, itemIDList, value, tourId) {
+ var popupContent = '' + response.title + ' ';
+ if (response.thumbnail) {
+ popupContent += '' + response.thumbnail + ' ';
+ }
+ popupContent += 'View More Info ';
+ if (!layer.getPopup()) {
+ marker.bindPopup(popupContent, { maxWidth: 200, offset: L.point(0, -40) }).openPopup();
+ allMarkers[response.id] = marker;
+ }
+
+ window.setTimeout(function () {
+ layer.getPopup().update()
+ $('.open-info-panel').click(function (e) {
+ e.preventDefault();
+ $('#info-panel-container').fadeToggle(200, 'linear');
+ $('#toggle-map-button + .back-button').show();
+ marker.closePopup();
+ });
+ }, 500);
+
+ // Populate the item info panel.
+ populatePopup(itemIDList, value, response, itemIDList.findIndex((ele) => ele == response.id), tourId);
+ }
+
+ function populateTourIntroPopup(itemIDList, value, tour_id) {
+ $('.next-button').unbind("click");
+ $('.prev-button').unbind("click");
+
+ $('.prev-button').addClass('off');
+ $('.next-button').addClass('off');
+
+ document.getElementById("info-panel-name").innerHTML = value["Tour Name"];
+ $('.panel-title').css("backgroundColor", value['Color'])
+ var content = $('#info-panel-content');
+ content.empty();
+
+ var infoContent = ""
+ var rightContent = "";
+ // click title to show the popup on map
+ if (value.Description != "") {
+ rightContent += '' + value.Description + '
'
+ } else {
+ rightContent += " No descriptions available.
"
+ }
+
+ route = JSON.parse(value.Route);
+ var distance = route.features[0].properties.summary.distance;
+ var duration = route.features[0].properties.summary.duration;
+
+ rightContent += 'Distance: ' + Math.round(distance / 10) / 100 + ' km
'
+ rightContent += 'Duration: ~' + Math.round(duration / 60) + ' min walk
'
+ rightContent += '
'
+
+ if (value.Credits != "") {
+ rightContent += " Credits "
+ rightContent += '' + value.Credits + '
'
+ }
+ rightContent += 'Start Tour
';
+ window.setTimeout(function () {
+ $('#start-tour').click(function (e) {
+ var newId = itemIDList[0]
+ popupButtonEvent(e, newId, itemIDList, value, tour_id);
+ })
+ }, 500)
+ infoContent += '';
+
+ content.append('' + infoContent + '
')
+ }
+
+ function populatePopup(itemIDList, value, response, numPopup, tour_id) {
+ var numPopup = itemIDList.findIndex((ele) => ele == response.id);
+ var coor = value.Data.features[numPopup].geometry.coordinates;
+ map.flyTo([coor[1], coor[0]], MAP_ZOOM + MAP_MAX_ZOOM_STOP);
+
+ $('.next-button').unbind("click");
+ $('.prev-button').unbind("click");
+
+ if (numPopup + 1 == itemIDList.length) {
+ $('.next-button').addClass('off');
+ } else {
+ $('.next-button').removeClass('off');
+ $('.next-button').unbind("click");
+ window.setTimeout(function () {
+ $('.next-button').click(function (e) {
+ var newId = itemIDList[numPopup + 1]
+ // value.allMarker[numPopup].closePopup();
+ popupButtonEvent(e, newId, itemIDList, value, tour_id);
+ })
+ }, 500)
+ }
+
+ if (numPopup - 1 == -1 || numPopup == -1) {
+ $('.prev-button').addClass('off');
+ } else {
+ $('.prev-button').removeClass('off');
+ window.setTimeout(function () {
+ $('.prev-button').click(function (e) {
+ var newId = itemIDList[numPopup - 1]
+ // value.allMarker[numPopup].closePopup();
+ popupButtonEvent(e, newId, itemIDList, value, tour_id);
+ })
+ }, 500)
+ }
+
+ document.getElementById("info-panel-name").innerHTML = value["Tour Name"] + ` #${numPopup + 1}`;
+ $('.panel-title').css("backgroundColor", value['Color'])
+ var content = $('#info-panel-content');
+ content.empty();
+
+ var infoContent = ""
+ var leftContent = "";
+ var rightContent = "";
+ if (response.fullsize) {
+ leftContent += response.fullsize;
+ infoContent += '' + leftContent + '
';
+ }
+
+ // click title to show the popup on map
+ rightContent += `` + response.title + ' '
+ if (response.abstract) {
+ rightContent += '' + response.abstract + '
';
+ } else if (response.description) {
+ rightContent += '' + response.description + '
';
+ } else {
+ rightContent += 'No descriptions available.
';
+ }
+ rightContent += ''
+ infoContent += '';
+
+ content.append('' + infoContent + '
')
+ }
+
+ function getMarkerHTML(color) {
+ let markerHtmlStyles = `
+ background-color: ${color};
+ width: 1.7rem;
+ height: 1.7rem;
+ display: block;
+ left: -0.5rem;
+ top: -0.5rem;
+ position: relative;
+ border-radius: 1.5rem 1.5rem 0;
+ transform: rotate(45deg);`
+ return markerHtmlStyles;
+ }
+
+ function popupButtonEvent(e, id, itemIDList, value, tour_id) {
+ e.preventDefault();
+ var response = allItems[`${tour_id}:${id}`]
+ if (response == undefined) {
+ $.post('walking-tour/index/get-item', { id: id, tour: tour_id }, function (response) {
+ allItems[`${tour_id}:${id}`] = response;
+ populatePopup(itemIDList, value, response, itemIDList.findIndex((ele) => ele == response.id), tour_id);
+ })
+ } else {
+ populatePopup(itemIDList, value, response, itemIDList.findIndex((ele) => ele == response.id), tour_id)
+ }
+ }
+
+
+ /*
+ * Add the historic map layer.
+ */
+ function addHistoricMapLayer() {
+ // Get the historic map data
+ var getData = { 'text': $('#map-coverage').val() };
+ $.get('walking-tour/index/historic-map-data', getData, function (response) {
+ historicMapLayer = L.tileLayer(
+ response.url,
+ { tms: true, opacity: 1.00 }
+ );
+ map.addLayer(historicMapLayer);
+ $('#toggle-map-button').show();
+
+ // Set the map title as the map attribution prefix.
+ map.attributionControl.setPrefix(response.title);
+ });
+ }
+
+ /*
+ * Remove the historic map layer.
+ */
+ function removeHistoricMapLayer() {
+ $('#toggle-map-button').data('clicks', false).hide();
+ map.removeLayer(historicMapLayer);
+ map.attributionControl.setPrefix('');
+ }
+
+ /*
+ * Helper Functions
+ */
+
+ function hexToRgb(hex) {
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16)
+ } : null;
+ }
+
+ function parse1DArrayPoint(text) {
+ ptn = text.split(",")
+ ptn.forEach(function (ele, index) {
+ temp_ele = ele.replace(/\s+/g, '');
+ temp_ele = temp_ele.replace('[', '');
+ temp_ele = temp_ele.replace(']', '');
+ this[index] = parseFloat(temp_ele)
+ }, ptn);
+ return ptn;
+ }
+
+ function parse2DArrayPoint(text) {
+ ptn = text.match(/(\[-?[0-9]*.[0-9]*, -?[0-9]*.[0-9]*\])/g)
+ b_new = []
+ ptn.forEach(ele => {
+ temp_ele = ele.split(",")
+ temp_ele.forEach(function (ele_inner, index_inner) {
+ temp_ele_inner = ele_inner.replace(/\s+/g, '');
+ temp_ele_inner = temp_ele_inner.replace('[', '');
+ temp_ele_inner = temp_ele_inner.replace(']', '');
+ this[index_inner] = parseFloat(temp_ele_inner)
+ }, temp_ele)
+ b_new.push(temp_ele)
+ })
+ return b_new
+ }
+
+ /*
+ * Revert to default (original) form state.
+ */
+ function revertFormState() {
+ if (historicMapLayer) {
+ removeHistoricMapLayer();
+ }
+
+ $('#map-coverage').val('0');
+ $('#tour-type').val('0');
+
+ $('#place-type-div').hide({ duration: 'fast' });
+ $('input[name=place-type-all]').prop('checked', true).
+ parent().addClass('on');
+ $('input[name=place-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+
+ $('#event-type-div').hide({ duration: 'fast' });
+ $('input[name=event-type-all]').prop('checked', true).
+ parent().addClass('on');
+ $('input[name=event-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+
+ doFilters();
+ }
+
+ /*
+ * Retain previous form state.
+ *
+ * Acts on the assumption that all browsers will preserve the form state
+ * when navigating back to the map from another page.
+ */
+ function retainFormState() {
+ if ('Place' == $('#tour-type').find(':selected').text()) {
+ var placeTypes = $('input[name=place-type]:checked');
+ if (placeTypes.length) {
+ $('input[name=place-type-all]').parent().removeClass('on');
+ placeTypes.parent().addClass('on');
+ }
+ $('#place-type-div').show({ duration: 'fast' });
+ }
+ if ('Event' == $('#tour-type').find(':selected').text()) {
+ var eventTypes = $('input[name=event-type]:checked');
+ if (eventTypes.length) {
+ $('input[name=event-type-all]').parent().removeClass('on');
+ eventTypes.parent().addClass('on');
+ }
+ $('#event-type-div').show({ duration: 'fast' });
+ }
+ }
+
+ /*
+ * Revert to default (original) form state.
+ */
+ function revertFormState() {
+ if (historicMapLayer) {
+ removeHistoricMapLayer();
+ }
+
+ $('#map-coverage').val('0');
+ $('#tour-type').val('0');
+
+ $('#place-type-div').hide({ duration: 'fast' });
+ $('input[name=place-type-all]').prop('checked', true).
+ parent().addClass('on');
+ $('input[name=place-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+
+ $('#event-type-div').hide({ duration: 'fast' });
+ $('input[name=event-type-all]').prop('checked', true).
+ parent().addClass('on');
+ $('input[name=event-type]:checked').prop('checked', false).
+ parent().removeClass('on');
+
+ doFilters();
+ }
+
+ /*
+ * Retain previous form state.
+ *
+ * Acts on the assumption that all browsers will preserve the form state
+ * when navigating back to the map from another page.
+ */
+ function retainFormState() {
+ if ('Place' == $('#tour-type').find(':selected').text()) {
+ var placeTypes = $('input[name=place-type]:checked');
+ if (placeTypes.length) {
+ $('input[name=place-type-all]').parent().removeClass('on');
+ placeTypes.parent().addClass('on');
+ }
+ $('#place-type-div').show({ duration: 'fast' });
+ }
+ if ('Event' == $('#tour-type').find(':selected').text()) {
+ var eventTypes = $('input[name=event-type]:checked');
+ if (eventTypes.length) {
+ $('input[name=event-type-all]').parent().removeClass('on');
+ eventTypes.parent().addClass('on');
+ }
+ $('#event-type-div').show({ duration: 'fast' });
+ }
+ }
+}
diff --git a/views/shared/javascripts/Polyline.encoded.js b/views/shared/javascripts/Polyline.encoded.js
new file mode 100644
index 0000000..957f6cb
--- /dev/null
+++ b/views/shared/javascripts/Polyline.encoded.js
@@ -0,0 +1,233 @@
+/*
+ * Utility functions to decode/encode numbers and array's of numbers
+ * to/from strings (Google maps polyline encoding)
+ *
+ * Extends the L.Polyline and L.Polygon object with methods to convert
+ * to and create from these strings.
+ *
+ * Jan Pieter Waagmeester
+ *
+ * Original code from:
+ * http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/
+ * (which is down as of december 2014)
+ */
+
+(function () {
+ 'use strict';
+
+ var defaultOptions = function (options) {
+ if (typeof options === 'number') {
+ // Legacy
+ options = {
+ precision: options
+ };
+ } else {
+ options = options || {};
+ }
+
+ options.precision = options.precision || 5;
+ options.factor = options.factor || Math.pow(10, options.precision);
+ options.dimension = options.dimension || 2;
+ return options;
+ };
+
+ var PolylineUtil = {
+ encode: function (points, options) {
+ options = defaultOptions(options);
+
+ var flatPoints = [];
+ for (var i = 0, len = points.length; i < len; ++i) {
+ var point = points[i];
+
+ if (options.dimension === 2) {
+ flatPoints.push(point.lat || point[0]);
+ flatPoints.push(point.lng || point[1]);
+ } else {
+ for (var dim = 0; dim < options.dimension; ++dim) {
+ flatPoints.push(point[dim]);
+ }
+ }
+ }
+
+ return this.encodeDeltas(flatPoints, options);
+ },
+
+ decode: function (encoded, options) {
+ options = defaultOptions(options);
+
+ var flatPoints = this.decodeDeltas(encoded, options);
+
+ var points = [];
+ for (var i = 0, len = flatPoints.length; i + (options.dimension - 1) < len;) {
+ var point = [];
+
+ for (var dim = 0; dim < options.dimension; ++dim) {
+ point.push(flatPoints[i++]);
+ }
+
+ points.push(point);
+ }
+
+ return points;
+ },
+
+ encodeDeltas: function (numbers, options) {
+ options = defaultOptions(options);
+
+ var lastNumbers = [];
+
+ for (var i = 0, len = numbers.length; i < len;) {
+ for (var d = 0; d < options.dimension; ++d, ++i) {
+ var num = numbers[i].toFixed(options.precision);
+ var delta = num - (lastNumbers[d] || 0);
+ lastNumbers[d] = num;
+
+ numbers[i] = delta;
+ }
+ }
+
+ return this.encodeFloats(numbers, options);
+ },
+
+ decodeDeltas: function (encoded, options) {
+ options = defaultOptions(options);
+
+ var lastNumbers = [];
+
+ var numbers = this.decodeFloats(encoded, options);
+ for (var i = 0, len = numbers.length; i < len;) {
+ for (var d = 0; d < options.dimension; ++d, ++i) {
+ numbers[i] = Math.round((lastNumbers[d] = numbers[i] + (lastNumbers[d] || 0)) * options.factor) / options.factor;
+ }
+ }
+
+ return numbers;
+ },
+
+ encodeFloats: function (numbers, options) {
+ options = defaultOptions(options);
+
+ for (var i = 0, len = numbers.length; i < len; ++i) {
+ numbers[i] = Math.round(numbers[i] * options.factor);
+ }
+
+ return this.encodeSignedIntegers(numbers);
+ },
+
+ decodeFloats: function (encoded, options) {
+ options = defaultOptions(options);
+
+ var numbers = this.decodeSignedIntegers(encoded);
+ for (var i = 0, len = numbers.length; i < len; ++i) {
+ numbers[i] /= options.factor;
+ }
+
+ return numbers;
+ },
+
+ encodeSignedIntegers: function (numbers) {
+ for (var i = 0, len = numbers.length; i < len; ++i) {
+ var num = numbers[i];
+ numbers[i] = (num < 0) ? ~(num << 1) : (num << 1);
+ }
+
+ return this.encodeUnsignedIntegers(numbers);
+ },
+
+ decodeSignedIntegers: function (encoded) {
+ var numbers = this.decodeUnsignedIntegers(encoded);
+
+ for (var i = 0, len = numbers.length; i < len; ++i) {
+ var num = numbers[i];
+ numbers[i] = (num & 1) ? ~(num >> 1) : (num >> 1);
+ }
+
+ return numbers;
+ },
+
+ encodeUnsignedIntegers: function (numbers) {
+ var encoded = '';
+ for (var i = 0, len = numbers.length; i < len; ++i) {
+ encoded += this.encodeUnsignedInteger(numbers[i]);
+ }
+ return encoded;
+ },
+
+ decodeUnsignedIntegers: function (encoded) {
+ var numbers = [];
+
+ var current = 0;
+ var shift = 0;
+
+ for (var i = 0, len = encoded.length; i < len; ++i) {
+ var b = encoded.charCodeAt(i) - 63;
+
+ current |= (b & 0x1f) << shift;
+
+ if (b < 0x20) {
+ numbers.push(current);
+ current = 0;
+ shift = 0;
+ } else {
+ shift += 5;
+ }
+ }
+
+ return numbers;
+ },
+
+ encodeSignedInteger: function (num) {
+ num = (num < 0) ? ~(num << 1) : (num << 1);
+ return this.encodeUnsignedInteger(num);
+ },
+
+ // This function is very similar to Google's, but I added
+ // some stuff to deal with the double slash issue.
+ encodeUnsignedInteger: function (num) {
+ var value, encoded = '';
+ while (num >= 0x20) {
+ value = (0x20 | (num & 0x1f)) + 63;
+ encoded += (String.fromCharCode(value));
+ num >>= 5;
+ }
+ value = num + 63;
+ encoded += (String.fromCharCode(value));
+
+ return encoded;
+ }
+ };
+
+ // Export Node module
+ if (typeof module === 'object' && typeof module.exports === 'object') {
+ module.exports = PolylineUtil;
+ }
+
+ // Inject functionality into Leaflet
+ if (typeof L === 'object') {
+ if (!(L.Polyline.prototype.fromEncoded)) {
+ L.Polyline.fromEncoded = function (encoded, options) {
+ return L.polyline(PolylineUtil.decode(encoded), options);
+ };
+ }
+ if (!(L.Polygon.prototype.fromEncoded)) {
+ L.Polygon.fromEncoded = function (encoded, options) {
+ return L.polygon(PolylineUtil.decode(encoded), options);
+ };
+ }
+
+ var encodeMixin = {
+ encodePath: function () {
+ return PolylineUtil.encode(this.getLatLngs());
+ }
+ };
+
+ if (!L.Polyline.prototype.encodePath) {
+ L.Polyline.include(encodeMixin);
+ }
+ if (!L.Polygon.prototype.encodePath) {
+ L.Polygon.include(encodeMixin);
+ }
+
+ L.PolylineUtil = PolylineUtil;
+ }
+})();
diff --git a/views/public/javascripts/jquery.cookie.js b/views/shared/javascripts/jquery.cookie.js
similarity index 100%
rename from views/public/javascripts/jquery.cookie.js
rename to views/shared/javascripts/jquery.cookie.js
diff --git a/views/shared/javascripts/jquery.ui.touch-punch.min.js b/views/shared/javascripts/jquery.ui.touch-punch.min.js
new file mode 100644
index 0000000..31272ce
--- /dev/null
+++ b/views/shared/javascripts/jquery.ui.touch-punch.min.js
@@ -0,0 +1,11 @@
+/*!
+ * jQuery UI Touch Punch 0.2.3
+ *
+ * Copyright 2011–2014, Dave Furfero
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ *
+ * Depends:
+ * jquery.ui.widget.js
+ * jquery.ui.mouse.js
+ */
+!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
\ No newline at end of file
diff --git a/views/public/javascripts/leaflet.markercluster-src.js b/views/shared/javascripts/leaflet.markercluster-src.js
old mode 100644
new mode 100755
similarity index 100%
rename from views/public/javascripts/leaflet.markercluster-src.js
rename to views/shared/javascripts/leaflet.markercluster-src.js
diff --git a/views/public/javascripts/leaflet.markercluster.js b/views/shared/javascripts/leaflet.markercluster.js
old mode 100644
new mode 100755
similarity index 99%
rename from views/public/javascripts/leaflet.markercluster.js
rename to views/shared/javascripts/leaflet.markercluster.js
index ad73311..db48b44
--- a/views/public/javascripts/leaflet.markercluster.js
+++ b/views/shared/javascripts/leaflet.markercluster.js
@@ -3,4 +3,4 @@
https://github.com/Leaflet/Leaflet.markercluster
(c) 2012-2013, Dave Leaver, smartrak
*/
-!function(t,e){L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,polygonOptions:{}},initialize:function(t){L.Util.setOptions(this,t),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.on(L.FeatureGroup.EVENTS,this._propagateEvent,this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.on(L.FeatureGroup.EVENTS,this._propagateEvent,this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null},addLayer:function(t){if(t instanceof L.LayerGroup){var e=[];for(var i in t._layers)e.push(t._layers[i]);return this.addLayers(e)}if(!t.getLatLng)return this._nonPointGroup.addLayer(t),this;if(!this._map)return this._needsClustering.push(t),this;if(this.hasLayer(t))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(t,this._maxZoom);var n=t,s=this._map.getZoom();if(t.__parent)for(;n.__parent._zoom>=s;)n=n.__parent;return this._currentShownBounds.contains(n.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(t,n):this._animationAddLayerNonAnimated(t,n)),this},removeLayer:function(t){if(t instanceof L.LayerGroup){var e=[];for(var i in t._layers)e.push(t._layers[i]);return this.removeLayers(e)}return t.getLatLng?this._map?t.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(t)),this._removeLayer(t,!0),this._featureGroup.hasLayer(t)&&(this._featureGroup.removeLayer(t),t.setOpacity&&t.setOpacity(1)),this):this:(!this._arraySplice(this._needsClustering,t)&&this.hasLayer(t)&&this._needsRemoving.push(t),this):(this._nonPointGroup.removeLayer(t),this)},addLayers:function(t){var e,i,n,s=this._map,r=this._featureGroup,o=this._nonPointGroup;for(e=0,i=t.length;i>e;e++)if(n=t[e],n.getLatLng){if(!this.hasLayer(n))if(s){if(this._addLayer(n,this._maxZoom),n.__parent&&2===n.__parent.getChildCount()){var a=n.__parent.getAllChildMarkers(),h=a[0]===n?a[1]:a[0];r.removeLayer(h)}}else this._needsClustering.push(n)}else o.addLayer(n);return s&&(r.eachLayer(function(t){t instanceof L.MarkerCluster&&t._iconNeedsUpdate&&t._updateIcon()}),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)),this},removeLayers:function(t){var e,i,n,s=this._featureGroup,r=this._nonPointGroup;if(!this._map){for(e=0,i=t.length;i>e;e++)n=t[e],this._arraySplice(this._needsClustering,n),r.removeLayer(n);return this}for(e=0,i=t.length;i>e;e++)n=t[e],n.__parent?(this._removeLayer(n,!0,!0),s.hasLayer(n)&&(s.removeLayer(n),n.setOpacity&&n.setOpacity(1))):r.removeLayer(n);return this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),s.eachLayer(function(t){t instanceof L.MarkerCluster&&t._updateIcon()}),this},clearLayers:function(){return this._map||(this._needsClustering=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(t){delete t.__parent}),this._map&&this._generateInitialClusters(),this},getBounds:function(){var t=new L.LatLngBounds;if(this._topClusterLevel)t.extend(this._topClusterLevel._bounds);else for(var e=this._needsClustering.length-1;e>=0;e--)t.extend(this._needsClustering[e].getLatLng());return t.extend(this._nonPointGroup.getBounds()),t},eachLayer:function(t,e){var i,n=this._needsClustering.slice();for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(n),i=n.length-1;i>=0;i--)t.call(e,n[i]);this._nonPointGroup.eachLayer(t,e)},getLayers:function(){var t=[];return this.eachLayer(function(e){t.push(e)}),t},hasLayer:function(t){if(!t)return!1;var e,i=this._needsClustering;for(e=i.length-1;e>=0;e--)if(i[e]===t)return!0;for(i=this._needsRemoving,e=i.length-1;e>=0;e--)if(i[e]===t)return!1;return!(!t.__parent||t.__parent._group!==this)||this._nonPointGroup.hasLayer(t)},zoomToShowLayer:function(t,e){var i=function(){if((t._icon||t.__parent._icon)&&!this._inZoomAnimation)if(this._map.off("moveend",i,this),this.off("animationend",i,this),t._icon)e();else if(t.__parent._icon){var n=function(){this.off("spiderfied",n,this),e()};this.on("spiderfied",n,this),t.__parent.spiderfy()}};t._icon?e():t.__parent._zoome;e++)n=this._needsRemoving[e],this._removeLayer(n,!0);for(this._needsRemoving=[],e=0,i=this._needsClustering.length;i>e;e++)n=this._needsClustering[e],n.getLatLng?n.__parent||this._addLayer(n,this._maxZoom):this._featureGroup.addLayer(n);this._needsClustering=[],this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),this._zoom=this._map.getZoom(),this._currentShownBounds=this._getExpandedVisibleBounds(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)},onRemove:function(t){t.off("zoomend",this._zoomEnd,this),t.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),this._hideCoverage(),this._featureGroup.onRemove(t),this._nonPointGroup.onRemove(t),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(t){for(var e=t;e&&!e._icon;)e=e.__parent;return e||null},_arraySplice:function(t,e){for(var i=t.length-1;i>=0;i--)if(t[i]===e)return t.splice(i,1),!0},_removeLayer:function(t,e,i){var n=this._gridClusters,s=this._gridUnclustered,r=this._featureGroup,o=this._map;if(e)for(var a=this._maxZoom;a>=0&&s[a].removeObject(t,o.project(t.getLatLng(),a));a--);var h,_=t.__parent,u=_._markers;for(this._arraySplice(u,t);_&&(_._childCount--,!(_._zoom<0));)e&&_._childCount<=1?(h=_._markers[0]===t?_._markers[1]:_._markers[0],n[_._zoom].removeObject(_,o.project(_._cLatLng,_._zoom)),s[_._zoom].addObject(h,o.project(h.getLatLng(),_._zoom)),this._arraySplice(_.__parent._childClusters,_),_.__parent._markers.push(h),h.__parent=_.__parent,_._icon&&(r.removeLayer(_),i||r.addLayer(h))):(_._recalculateBounds(),i&&_._icon||_._updateIcon()),_=_.__parent;delete t.__parent},_propagateEvent:function(t){t.layer instanceof L.MarkerCluster&&(t.type="cluster"+t.type),this.fire(t.type,t)},_defaultIconCreateFunction:function(t){var e=t.getChildCount(),i=" marker-cluster-";return i+=10>e?"small":100>e?"medium":"large",new L.DivIcon({html:""+e+"
",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var t=this._map,e=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(e||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),t.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(t){var e=this._map;e.getMaxZoom()===e.getZoom()?this.options.spiderfyOnMaxZoom&&t.layer.spiderfy():this.options.zoomToBoundsOnClick&&t.layer.zoomToBounds()},_showCoverage:function(t){var e=this._map;this._inZoomAnimation||(this._shownPolygon&&e.removeLayer(this._shownPolygon),t.layer.getChildCount()>2&&t.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(t.layer.getConvexHull(),this.options.polygonOptions),e.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var t=this.options.spiderfyOnMaxZoom,e=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(t||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),e&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=this._map._zoom,this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var t=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._zoom,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,t),this._currentShownBounds=t}},_generateInitialClusters:function(){var t=this._map.getMaxZoom(),e=this.options.maxClusterRadius;this.options.disableClusteringAtZoom&&(t=this.options.disableClusteringAtZoom-1),this._maxZoom=t,this._gridClusters={},this._gridUnclustered={};for(var i=t;i>=0;i--)this._gridClusters[i]=new L.DistanceGrid(e),this._gridUnclustered[i]=new L.DistanceGrid(e);this._topClusterLevel=new L.MarkerCluster(this,-1)},_addLayer:function(t,e){var i,n,s=this._gridClusters,r=this._gridUnclustered;for(this.options.singleMarkerMode&&(t.options.icon=this.options.iconCreateFunction({getChildCount:function(){return 1},getAllChildMarkers:function(){return[t]}}));e>=0;e--){i=this._map.project(t.getLatLng(),e);var o=s[e].getNearObject(i);if(o)return o._addChild(t),t.__parent=o,void 0;if(o=r[e].getNearObject(i)){var a=o.__parent;a&&this._removeLayer(o,!1);var h=new L.MarkerCluster(this,e,o,t);s[e].addObject(h,this._map.project(h._cLatLng,e)),o.__parent=h,t.__parent=h;var _=h;for(n=e-1;n>a._zoom;n--)_=new L.MarkerCluster(this,n,_),s[n].addObject(_,this._map.project(o.getLatLng(),n));for(a._addChild(_),n=e;n>=0&&r[n].removeObject(o,this._map.project(o.getLatLng(),n));n--);return}r[e].addObject(t,i)}this._topClusterLevel._addChild(t),t.__parent=this._topClusterLevel},_mergeSplitClusters:function(){this._zoomthis._map._zoom?(this._animationStart(),this._animationZoomOut(this._zoom,this._map._zoom)):this._moveEnd()},_getExpandedVisibleBounds:function(){if(!this.options.removeOutsideVisibleBounds)return this.getBounds();var t=this._map,e=t.getBounds(),i=e._southWest,n=e._northEast,s=L.Browser.mobile?0:Math.abs(i.lat-n.lat),r=L.Browser.mobile?0:Math.abs(i.lng-n.lng);return new L.LatLngBounds(new L.LatLng(i.lat-s,i.lng-r,!0),new L.LatLng(n.lat+s,n.lng+r,!0))},_animationAddLayerNonAnimated:function(t,e){if(e===t)this._featureGroup.addLayer(t);else if(2===e._childCount){e._addToMap();var i=e.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else e._updateIcon()}}),L.MarkerClusterGroup.include(L.DomUtil.TRANSITION?{_animationStart:function(){this._map._mapPane.className+=" leaflet-cluster-anim",this._inZoomAnimation++},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_animationZoomIn:function(t,e){var i,n=this,s=this._getExpandedVisibleBounds(),r=this._featureGroup;this._topClusterLevel._recursively(s,t,0,function(n){var o,a=n._latlng,h=n._markers;for(s.contains(a)||(a=null),n._isSingleParent()&&t+1===e?(r.removeLayer(n),n._recursivelyAddChildrenToMap(null,e,s)):(n.setOpacity(0),n._recursivelyAddChildrenToMap(a,e,s)),i=h.length-1;i>=0;i--)o=h[i],s.contains(o._latlng)||r.removeLayer(o)}),this._forceLayout(),n._topClusterLevel._recursivelyBecomeVisible(s,e),r.eachLayer(function(t){t instanceof L.MarkerCluster||!t._icon||t.setOpacity(1)}),n._topClusterLevel._recursively(s,t,e,function(t){t._recursivelyRestoreChildPositions(e)}),setTimeout(function(){n._topClusterLevel._recursively(s,t,0,function(t){r.removeLayer(t),t.setOpacity(1)}),n._animationEnd()},200)},_animationZoomOut:function(t,e){this._animationZoomOutSingle(this._topClusterLevel,t-1,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t,this._getExpandedVisibleBounds())},_animationZoomOutSingle:function(t,e,i){var n=this._getExpandedVisibleBounds();t._recursivelyAnimateChildrenInAndAddSelfToMap(n,e+1,i);var s=this;this._forceLayout(),t._recursivelyBecomeVisible(n,i),setTimeout(function(){if(1===t._childCount){var r=t._markers[0];r.setLatLng(r.getLatLng()),r.setOpacity(1)}else t._recursively(n,i,0,function(t){t._recursivelyRemoveChildrenFromMap(n,e+1)});s._animationEnd()},200)},_animationAddLayer:function(t,e){var i=this,n=this._featureGroup;n.addLayer(t),e!==t&&(e._childCount>2?(e._updateIcon(),this._forceLayout(),this._animationStart(),t._setPos(this._map.latLngToLayerPoint(e.getLatLng())),t.setOpacity(0),setTimeout(function(){n.removeLayer(t),t.setOpacity(1),i._animationEnd()},200)):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(e,this._map.getMaxZoom(),this._map.getZoom())))},_forceLayout:function(){L.Util.falseFn(e.body.offsetWidth)}}:{_animationStart:function(){},_animationZoomIn:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds())},_animationZoomOut:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds())},_animationAddLayer:function(t,e){this._animationAddLayerNonAnimated(t,e)}}),L.markerClusterGroup=function(t){return new L.MarkerClusterGroup(t)},L.MarkerCluster=L.Marker.extend({initialize:function(t,e,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this}),this._group=t,this._zoom=e,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(t){t=t||[];for(var e=this._childClusters.length-1;e>=0;e--)this._childClusters[e].getAllChildMarkers(t);for(var i=this._markers.length-1;i>=0;i--)t.push(this._markers[i]);return t},getChildCount:function(){return this._childCount},zoomToBounds:function(){for(var t,e=this._childClusters.slice(),i=this._group._map,n=i.getBoundsZoom(this._bounds),s=this._zoom+1;e.length>0&&n>s;){s++;var r=[];for(t=0;ts?this._group._map.setView(this._latlng,s):this._group._map.fitBounds(this._bounds)},getBounds:function(){var t=new L.LatLngBounds;return t.extend(this._bounds),t},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(t,e){this._iconNeedsUpdate=!0,this._expandBounds(t),t instanceof L.MarkerCluster?(e||(this._childClusters.push(t),t.__parent=this),this._childCount+=t._childCount):(e||this._markers.push(t),this._childCount++),this.__parent&&this.__parent._addChild(t,!0)},_expandBounds:function(t){var e,i=t._wLatLng||t._latlng;t instanceof L.MarkerCluster?(this._bounds.extend(t._bounds),e=t._childCount):(this._bounds.extend(i),e=1),this._cLatLng||(this._cLatLng=t._cLatLng||i);var n=this._childCount+e;this._wLatLng?(this._wLatLng.lat=(i.lat*e+this._wLatLng.lat*this._childCount)/n,this._wLatLng.lng=(i.lng*e+this._wLatLng.lng*this._childCount)/n):this._latlng=this._wLatLng=new L.LatLng(i.lat,i.lng)},_addToMap:function(t){t&&(this._backupLatlng=this._latlng,this.setLatLng(t)),this._group._featureGroup.addLayer(this)},_recursivelyAnimateChildrenIn:function(t,e,i){this._recursively(t,0,i-1,function(t){var i,n,s=t._markers;for(i=s.length-1;i>=0;i--)n=s[i],n._icon&&(n._setPos(e),n.setOpacity(0))},function(t){var i,n,s=t._childClusters;for(i=s.length-1;i>=0;i--)n=s[i],n._icon&&(n._setPos(e),n.setOpacity(0))})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(t,e,i){this._recursively(t,i,0,function(n){n._recursivelyAnimateChildrenIn(t,n._group._map.latLngToLayerPoint(n.getLatLng()).round(),e),n._isSingleParent()&&e-1===i?(n.setOpacity(1),n._recursivelyRemoveChildrenFromMap(t,e)):n.setOpacity(0),n._addToMap()})},_recursivelyBecomeVisible:function(t,e){this._recursively(t,0,e,null,function(t){t.setOpacity(1)})},_recursivelyAddChildrenToMap:function(t,e,i){this._recursively(i,-1,e,function(n){if(e!==n._zoom)for(var s=n._markers.length-1;s>=0;s--){var r=n._markers[s];i.contains(r._latlng)&&(t&&(r._backupLatlng=r.getLatLng(),r.setLatLng(t),r.setOpacity&&r.setOpacity(0)),n._group._featureGroup.addLayer(r))}},function(e){e._addToMap(t)})},_recursivelyRestoreChildPositions:function(t){for(var e=this._markers.length-1;e>=0;e--){var i=this._markers[e];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(t-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var s=this._childClusters.length-1;s>=0;s--)this._childClusters[s]._recursivelyRestoreChildPositions(t)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(t,e,i){var n,s;this._recursively(t,-1,e-1,function(t){for(s=t._markers.length-1;s>=0;s--)n=t._markers[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.setOpacity&&n.setOpacity(1))},function(t){for(s=t._childClusters.length-1;s>=0;s--)n=t._childClusters[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.setOpacity&&n.setOpacity(1))})},_recursively:function(t,e,i,n,s){var r,o,a=this._childClusters,h=this._zoom;if(e>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s);else if(n&&n(this),s&&this._zoom===i&&s(this),i>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s)},_recalculateBounds:function(){var t,e=this._markers,i=this._childClusters;for(this._bounds=new L.LatLngBounds,delete this._wLatLng,t=e.length-1;t>=0;t--)this._expandBounds(e[t]);for(t=i.length-1;t>=0;t--)this._expandBounds(i[t])},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}}),L.DistanceGrid=function(t){this._cellSize=t,this._sqCellSize=t*t,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(t,e){var i=this._getCoord(e.x),n=this._getCoord(e.y),s=this._grid,r=s[n]=s[n]||{},o=r[i]=r[i]||[],a=L.Util.stamp(t);this._objectPoint[a]=e,o.push(t)},updateObject:function(t,e){this.removeObject(t),this.addObject(t,e)},removeObject:function(t,e){var i,n,s=this._getCoord(e.x),r=this._getCoord(e.y),o=this._grid,a=o[r]=o[r]||{},h=a[s]=a[s]||[];for(delete this._objectPoint[L.Util.stamp(t)],i=0,n=h.length;n>i;i++)if(h[i]===t)return h.splice(i,1),1===n&&delete a[s],!0},eachObject:function(t,e){var i,n,s,r,o,a,h,_=this._grid;for(i in _){o=_[i];for(n in o)for(a=o[n],s=0,r=a.length;r>s;s++)h=t.call(e,a[s]),h&&(s--,r--)}},getNearObject:function(t){var e,i,n,s,r,o,a,h,_=this._getCoord(t.x),u=this._getCoord(t.y),l=this._objectPoint,d=this._sqCellSize,p=null;for(e=u-1;u+1>=e;e++)if(s=this._grid[e])for(i=_-1;_+1>=i;i++)if(r=s[i])for(n=0,o=r.length;o>n;n++)a=r[n],h=this._sqDist(l[L.Util.stamp(a)],t),d>h&&(d=h,p=a);return p},_getCoord:function(t){return Math.floor(t/this._cellSize)},_sqDist:function(t,e){var i=e.x-t.x,n=e.y-t.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(t,e){var i=e[1].lat-e[0].lat,n=e[0].lng-e[1].lng;return n*(t.lat-e[0].lat)+i*(t.lng-e[0].lng)},findMostDistantPointFromBaseLine:function(t,e){var i,n,s,r=0,o=null,a=[];for(i=e.length-1;i>=0;i--)n=e[i],s=this.getDistant(n,t),s>0&&(a.push(n),s>r&&(r=s,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(t,e){var i=[],n=this.findMostDistantPointFromBaseLine(t,e);return n.maxPoint?(i=i.concat(this.buildConvexHull([t[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,t[1]],n.newPoints))):[t[0]]},getConvexHull:function(t){var e,i=!1,n=!1,s=null,r=null;for(e=t.length-1;e>=0;e--){var o=t[e];(i===!1||o.lat>i)&&(s=o,i=o.lat),(n===!1||o.lat=0;e--)t=i[e].getLatLng(),n.push(t);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:Math.PI/6,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var t,e=this.getAllChildMarkers(),i=this._group,n=i._map,s=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,e.length>=this._circleSpiralSwitchover?t=this._generatePointsSpiral(e.length,s):(s.y+=10,t=this._generatePointsCircle(e.length,s)),this._animationSpiderfy(e,t)}},unspiderfy:function(t){this._group._inZoomAnimation||(this._animationUnspiderfy(t),this._group._spiderfied=null)},_generatePointsCircle:function(t,e){var i,n,s=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+t),r=s/this._2PI,o=this._2PI/t,a=[];for(a.length=t,i=t-1;i>=0;i--)n=this._circleStartAngle+i*o,a[i]=new L.Point(e.x+r*Math.cos(n),e.y+r*Math.sin(n))._round();return a},_generatePointsSpiral:function(t,e){var i,n=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthStart,s=this._group.options.spiderfyDistanceMultiplier*this._spiralFootSeparation,r=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthFactor,o=0,a=[];for(a.length=t,i=t-1;i>=0;i--)o+=s/n+5e-4*i,a[i]=new L.Point(e.x+n*Math.cos(o),e.y+n*Math.sin(o))._round(),n+=this._2PI*r/o;return a},_noanimationUnspiderfy:function(){var t,e,i=this._group,n=i._map,s=i._featureGroup,r=this.getAllChildMarkers();for(this.setOpacity(1),e=r.length-1;e>=0;e--)t=r[e],s.removeLayer(t),t._preSpiderfyLatlng&&(t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng),t.setZIndexOffset&&t.setZIndexOffset(0),t._spiderLeg&&(n.removeLayer(t._spiderLeg),delete t._spiderLeg);i._spiderfied=null}}),L.MarkerCluster.include(L.DomUtil.TRANSITION?{SVG_ANIMATION:function(){return e.createElementNS("http://www.w3.org/2000/svg","animate").toString().indexOf("SVGAnimate")>-1}(),_animationSpiderfy:function(t,i){var n,s,r,o,a=this,h=this._group,_=h._map,u=h._featureGroup,l=_.latLngToLayerPoint(this._latlng);for(n=t.length-1;n>=0;n--)s=t[n],s.setOpacity?(s.setZIndexOffset(1e6),s.setOpacity(0),u.addLayer(s),s._setPos(l)):u.addLayer(s);h._forceLayout(),h._animationStart();var d=L.Path.SVG?0:.3,p=L.Path.SVG_NS;for(n=t.length-1;n>=0;n--)if(o=_.layerPointToLatLng(i[n]),s=t[n],s._preSpiderfyLatlng=s._latlng,s.setLatLng(o),s.setOpacity&&s.setOpacity(1),r=new L.Polyline([a._latlng,o],{weight:1.5,color:"#222",opacity:d}),_.addLayer(r),s._spiderLeg=r,L.Path.SVG&&this.SVG_ANIMATION){var c=r._path.getTotalLength();r._path.setAttribute("stroke-dasharray",c+","+c);var m=e.createElementNS(p,"animate");m.setAttribute("attributeName","stroke-dashoffset"),m.setAttribute("begin","indefinite"),m.setAttribute("from",c),m.setAttribute("to",0),m.setAttribute("dur",.25),r._path.appendChild(m),m.beginElement(),m=e.createElementNS(p,"animate"),m.setAttribute("attributeName","stroke-opacity"),m.setAttribute("attributeName","stroke-opacity"),m.setAttribute("begin","indefinite"),m.setAttribute("from",0),m.setAttribute("to",.5),m.setAttribute("dur",.25),r._path.appendChild(m),m.beginElement()}if(a.setOpacity(.3),L.Path.SVG)for(this._group._forceLayout(),n=t.length-1;n>=0;n--)s=t[n]._spiderLeg,s.options.opacity=.5,s._path.setAttribute("stroke-opacity",.5);setTimeout(function(){h._animationEnd(),h.fire("spiderfied")},200)},_animationUnspiderfy:function(t){var e,i,n,s=this._group,r=s._map,o=s._featureGroup,a=t?r._latLngToNewLayerPoint(this._latlng,t.zoom,t.center):r.latLngToLayerPoint(this._latlng),h=this.getAllChildMarkers(),_=L.Path.SVG&&this.SVG_ANIMATION;for(s._animationStart(),this.setOpacity(1),i=h.length-1;i>=0;i--)e=h[i],e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng,e.setOpacity?(e._setPos(a),e.setOpacity(0)):o.removeLayer(e),_&&(n=e._spiderLeg._path.childNodes[0],n.setAttribute("to",n.getAttribute("from")),n.setAttribute("from",0),n.beginElement(),n=e._spiderLeg._path.childNodes[1],n.setAttribute("from",.5),n.setAttribute("to",0),n.setAttribute("stroke-opacity",0),n.beginElement(),e._spiderLeg._path.setAttribute("stroke-opacity",0)));setTimeout(function(){var t=0;for(i=h.length-1;i>=0;i--)e=h[i],e._spiderLeg&&t++;for(i=h.length-1;i>=0;i--)e=h[i],e._spiderLeg&&(e.setOpacity&&(e.setOpacity(1),e.setZIndexOffset(0)),t>1&&o.removeLayer(e),r.removeLayer(e._spiderLeg),delete e._spiderLeg);s._animationEnd()},200)}}:{_animationSpiderfy:function(t,e){var i,n,s,r,o=this._group,a=o._map,h=o._featureGroup;for(i=t.length-1;i>=0;i--)r=a.layerPointToLatLng(e[i]),n=t[i],n._preSpiderfyLatlng=n._latlng,n.setLatLng(r),n.setZIndexOffset&&n.setZIndexOffset(1e6),h.addLayer(n),s=new L.Polyline([this._latlng,r],{weight:1.5,color:"#222"}),a.addLayer(s),n._spiderLeg=s;this.setOpacity(.3),o.fire("spiderfied")},_animationUnspiderfy:function(){this._noanimationUnspiderfy()}}),L.MarkerClusterGroup.include({_spiderfied:null,_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Path.SVG&&!L.Browser.touch&&this._map._initPathRoot()},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy()},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(t){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(t))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(t){this._spiderfied&&this._spiderfied.unspiderfy(t)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(t){t._spiderLeg&&(this._featureGroup.removeLayer(t),t.setOpacity(1),t.setZIndexOffset(0),this._map.removeLayer(t._spiderLeg),delete t._spiderLeg)}})}(window,document);
\ No newline at end of file
+!function(t,e){L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,polygonOptions:{}},initialize:function(t){L.Util.setOptions(this,t),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.on(L.FeatureGroup.EVENTS,this._propagateEvent,this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.on(L.FeatureGroup.EVENTS,this._propagateEvent,this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null},addLayer:function(t){if(t instanceof L.LayerGroup){var e=[];for(var i in t._layers)e.push(t._layers[i]);return this.addLayers(e)}if(!t.getLatLng)return this._nonPointGroup.addLayer(t),this;if(!this._map)return this._needsClustering.push(t),this;if(this.hasLayer(t))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(t,this._maxZoom);var n=t,s=this._map.getZoom();if(t.__parent)for(;n.__parent._zoom>=s;)n=n.__parent;return this._currentShownBounds.contains(n.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(t,n):this._animationAddLayerNonAnimated(t,n)),this},removeLayer:function(t){if(t instanceof L.LayerGroup){var e=[];for(var i in t._layers)e.push(t._layers[i]);return this.removeLayers(e)}return t.getLatLng?this._map?t.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(t)),this._removeLayer(t,!0),this._featureGroup.hasLayer(t)&&(this._featureGroup.removeLayer(t),t.setOpacity&&t.setOpacity(1)),this):this:(!this._arraySplice(this._needsClustering,t)&&this.hasLayer(t)&&this._needsRemoving.push(t),this):(this._nonPointGroup.removeLayer(t),this)},addLayers:function(t){var e,i,n,s=this._map,r=this._featureGroup,o=this._nonPointGroup;for(e=0,i=t.length;i>e;e++)if(n=t[e],n.getLatLng){if(!this.hasLayer(n))if(s){if(this._addLayer(n,this._maxZoom),n.__parent&&2===n.__parent.getChildCount()){var a=n.__parent.getAllChildMarkers(),h=a[0]===n?a[1]:a[0];r.removeLayer(h)}}else this._needsClustering.push(n)}else o.addLayer(n);return s&&(r.eachLayer(function(t){t instanceof L.MarkerCluster&&t._iconNeedsUpdate&&t._updateIcon()}),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)),this},removeLayers:function(t){var e,i,n,s=this._featureGroup,r=this._nonPointGroup;if(!this._map){for(e=0,i=t.length;i>e;e++)n=t[e],this._arraySplice(this._needsClustering,n),r.removeLayer(n);return this}for(e=0,i=t.length;i>e;e++)n=t[e],n.__parent?(this._removeLayer(n,!0,!0),s.hasLayer(n)&&(s.removeLayer(n),n.setOpacity&&n.setOpacity(1))):r.removeLayer(n);return this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),s.eachLayer(function(t){t instanceof L.MarkerCluster&&t._updateIcon()}),this},clearLayers:function(){return this._map||(this._needsClustering=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(t){delete t.__parent}),this._map&&this._generateInitialClusters(),this},getBounds:function(){var t=new L.LatLngBounds;if(this._topClusterLevel)t.extend(this._topClusterLevel._bounds);else for(var e=this._needsClustering.length-1;e>=0;e--)t.extend(this._needsClustering[e].getLatLng());return t.extend(this._nonPointGroup.getBounds()),t},eachLayer:function(t,e){var i,n=this._needsClustering.slice();for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(n),i=n.length-1;i>=0;i--)t.call(e,n[i]);this._nonPointGroup.eachLayer(t,e)},getLayers:function(){var t=[];return this.eachLayer(function(e){t.push(e)}),t},hasLayer:function(t){if(!t)return!1;var e,i=this._needsClustering;for(e=i.length-1;e>=0;e--)if(i[e]===t)return!0;for(i=this._needsRemoving,e=i.length-1;e>=0;e--)if(i[e]===t)return!1;return!(!t.__parent||t.__parent._group!==this)||this._nonPointGroup.hasLayer(t)},zoomToShowLayer:function(t,e){var i=function(){if((t._icon||t.__parent._icon)&&!this._inZoomAnimation)if(this._map.off("moveend",i,this),this.off("animationend",i,this),t._icon)e();else if(t.__parent._icon){var n=function(){this.off("spiderfied",n,this),e()};this.on("spiderfied",n,this),t.__parent.spiderfy()}};t._icon?e():t.__parent._zoome;e++)n=this._needsRemoving[e],this._removeLayer(n,!0);for(this._needsRemoving=[],e=0,i=this._needsClustering.length;i>e;e++)n=this._needsClustering[e],n.getLatLng?n.__parent||this._addLayer(n,this._maxZoom):this._featureGroup.addLayer(n);this._needsClustering=[],this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),this._zoom=this._map.getZoom(),this._currentShownBounds=this._getExpandedVisibleBounds(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)},onRemove:function(t){t.off("zoomend",this._zoomEnd,this),t.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),this._hideCoverage(),this._featureGroup.onRemove(t),this._nonPointGroup.onRemove(t),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(t){for(var e=t;e&&!e._icon;)e=e.__parent;return e||null},_arraySplice:function(t,e){for(var i=t.length-1;i>=0;i--)if(t[i]===e)return t.splice(i,1),!0},_removeLayer:function(t,e,i){var n=this._gridClusters,s=this._gridUnclustered,r=this._featureGroup,o=this._map;if(e)for(var a=this._maxZoom;a>=0&&s[a].removeObject(t,o.project(t.getLatLng(),a));a--);var h,_=t.__parent,u=_._markers;for(this._arraySplice(u,t);_&&(_._childCount--,!(_._zoom<0));)e&&_._childCount<=1?(h=_._markers[0]===t?_._markers[1]:_._markers[0],n[_._zoom].removeObject(_,o.project(_._cLatLng,_._zoom)),s[_._zoom].addObject(h,o.project(h.getLatLng(),_._zoom)),this._arraySplice(_.__parent._childClusters,_),_.__parent._markers.push(h),h.__parent=_.__parent,_._icon&&(r.removeLayer(_),i||r.addLayer(h))):(_._recalculateBounds(),i&&_._icon||_._updateIcon()),_=_.__parent;delete t.__parent},_propagateEvent:function(t){t.layer instanceof L.MarkerCluster&&(t.type="cluster"+t.type),this.fire(t.type,t)},_defaultIconCreateFunction:function(t){var e=t.getChildCount(),i=" marker-cluster-";return i+=10>e?"small":100>e?"medium":"large",new L.DivIcon({html:""+e+"
",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var t=this._map,e=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(e||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),t.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(t){var e=this._map;e.getMaxZoom()===e.getZoom()?this.options.spiderfyOnMaxZoom&&t.layer.spiderfy():this.options.zoomToBoundsOnClick&&t.layer.zoomToBounds()},_showCoverage:function(t){var e=this._map;this._inZoomAnimation||(this._shownPolygon&&e.removeLayer(this._shownPolygon),t.layer.getChildCount()>2&&t.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(t.layer.getConvexHull(),this.options.polygonOptions),e.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var t=this.options.spiderfyOnMaxZoom,e=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(t||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),e&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=this._map._zoom,this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var t=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._zoom,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,t),this._currentShownBounds=t}},_generateInitialClusters:function(){var t=this._map.getMaxZoom(),e=this.options.maxClusterRadius;this.options.disableClusteringAtZoom&&(t=this.options.disableClusteringAtZoom-1),this._maxZoom=t,this._gridClusters={},this._gridUnclustered={};for(var i=t;i>=0;i--)this._gridClusters[i]=new L.DistanceGrid(e),this._gridUnclustered[i]=new L.DistanceGrid(e);this._topClusterLevel=new L.MarkerCluster(this,-1)},_addLayer:function(t,e){var i,n,s=this._gridClusters,r=this._gridUnclustered;for(this.options.singleMarkerMode&&(t.options.icon=this.options.iconCreateFunction({getChildCount:function(){return 1},getAllChildMarkers:function(){return[t]}}));e>=0;e--){i=this._map.project(t.getLatLng(),e);var o=s[e].getNearObject(i);if(o)return o._addChild(t),t.__parent=o,void 0;if(o=r[e].getNearObject(i)){var a=o.__parent;a&&this._removeLayer(o,!1);var h=new L.MarkerCluster(this,e,o,t);s[e].addObject(h,this._map.project(h._cLatLng,e)),o.__parent=h,t.__parent=h;var _=h;for(n=e-1;n>a._zoom;n--)_=new L.MarkerCluster(this,n,_),s[n].addObject(_,this._map.project(o.getLatLng(),n));for(a._addChild(_),n=e;n>=0&&r[n].removeObject(o,this._map.project(o.getLatLng(),n));n--);return}r[e].addObject(t,i)}this._topClusterLevel._addChild(t),t.__parent=this._topClusterLevel},_mergeSplitClusters:function(){this._zoomthis._map._zoom?(this._animationStart(),this._animationZoomOut(this._zoom,this._map._zoom)):this._moveEnd()},_getExpandedVisibleBounds:function(){if(!this.options.removeOutsideVisibleBounds)return this.getBounds();var t=this._map,e=t.getBounds(),i=e._southWest,n=e._northEast,s=L.Browser.mobile?0:Math.abs(i.lat-n.lat),r=L.Browser.mobile?0:Math.abs(i.lng-n.lng);return new L.LatLngBounds(new L.LatLng(i.lat-s,i.lng-r,!0),new L.LatLng(n.lat+s,n.lng+r,!0))},_animationAddLayerNonAnimated:function(t,e){if(e===t)this._featureGroup.addLayer(t);else if(2===e._childCount){e._addToMap();var i=e.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else e._updateIcon()}}),L.MarkerClusterGroup.include(L.DomUtil.TRANSITION?{_animationStart:function(){this._map._mapPane.className+=" leaflet-cluster-anim",this._inZoomAnimation++},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_animationZoomIn:function(t,e){var i,n=this,s=this._getExpandedVisibleBounds(),r=this._featureGroup;this._topClusterLevel._recursively(s,t,0,function(n){var o,a=n._latlng,h=n._markers;for(s.contains(a)||(a=null),n._isSingleParent()&&t+1===e?(r.removeLayer(n),n._recursivelyAddChildrenToMap(null,e,s)):(n.setOpacity(0),n._recursivelyAddChildrenToMap(a,e,s)),i=h.length-1;i>=0;i--)o=h[i],s.contains(o._latlng)||r.removeLayer(o)}),this._forceLayout(),n._topClusterLevel._recursivelyBecomeVisible(s,e),r.eachLayer(function(t){t instanceof L.MarkerCluster||!t._icon||t.setOpacity(1)}),n._topClusterLevel._recursively(s,t,e,function(t){t._recursivelyRestoreChildPositions(e)}),setTimeout(function(){n._topClusterLevel._recursively(s,t,0,function(t){r.removeLayer(t),t.setOpacity(1)}),n._animationEnd()},200)},_animationZoomOut:function(t,e){this._animationZoomOutSingle(this._topClusterLevel,t-1,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t,this._getExpandedVisibleBounds())},_animationZoomOutSingle:function(t,e,i){var n=this._getExpandedVisibleBounds();t._recursivelyAnimateChildrenInAndAddSelfToMap(n,e+1,i);var s=this;this._forceLayout(),t._recursivelyBecomeVisible(n,i),setTimeout(function(){if(1===t._childCount){var r=t._markers[0];r.setLatLng(r.getLatLng()),r.setOpacity(1)}else t._recursively(n,i,0,function(t){t._recursivelyRemoveChildrenFromMap(n,e+1)});s._animationEnd()},200)},_animationAddLayer:function(t,e){var i=this,n=this._featureGroup;n.addLayer(t),e!==t&&(e._childCount>2?(e._updateIcon(),this._forceLayout(),this._animationStart(),t._setPos(this._map.latLngToLayerPoint(e.getLatLng())),t.setOpacity(0),setTimeout(function(){n.removeLayer(t),t.setOpacity(1),i._animationEnd()},200)):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(e,this._map.getMaxZoom(),this._map.getZoom())))},_forceLayout:function(){L.Util.falseFn(e.body.offsetWidth)}}:{_animationStart:function(){},_animationZoomIn:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds())},_animationZoomOut:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds())},_animationAddLayer:function(t,e){this._animationAddLayerNonAnimated(t,e)}}),L.markerClusterGroup=function(t){return new L.MarkerClusterGroup(t)},L.MarkerCluster=L.Marker.extend({initialize:function(t,e,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this}),this._group=t,this._zoom=e,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(t){t=t||[];for(var e=this._childClusters.length-1;e>=0;e--)this._childClusters[e].getAllChildMarkers(t);for(var i=this._markers.length-1;i>=0;i--)t.push(this._markers[i]);return t},getChildCount:function(){return this._childCount},zoomToBounds:function(){for(var t,e=this._childClusters.slice(),i=this._group._map,n=i.getBoundsZoom(this._bounds),s=this._zoom+1;e.length>0&&n>s;){s++;var r=[];for(t=0;ts?this._group._map.setView(this._latlng,s):this._group._map.fitBounds(this._bounds)},getBounds:function(){var t=new L.LatLngBounds;return t.extend(this._bounds),t},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(t,e){this._iconNeedsUpdate=!0,this._expandBounds(t),t instanceof L.MarkerCluster?(e||(this._childClusters.push(t),t.__parent=this),this._childCount+=t._childCount):(e||this._markers.push(t),this._childCount++),this.__parent&&this.__parent._addChild(t,!0)},_expandBounds:function(t){var e,i=t._wLatLng||t._latlng;t instanceof L.MarkerCluster?(this._bounds.extend(t._bounds),e=t._childCount):(this._bounds.extend(i),e=1),this._cLatLng||(this._cLatLng=t._cLatLng||i);var n=this._childCount+e;this._wLatLng?(this._wLatLng.lat=(i.lat*e+this._wLatLng.lat*this._childCount)/n,this._wLatLng.lng=(i.lng*e+this._wLatLng.lng*this._childCount)/n):this._latlng=this._wLatLng=new L.LatLng(i.lat,i.lng)},_addToMap:function(t){t&&(this._backupLatlng=this._latlng,this.setLatLng(t)),this._group._featureGroup.addLayer(this)},_recursivelyAnimateChildrenIn:function(t,e,i){this._recursively(t,0,i-1,function(t){var i,n,s=t._markers;for(i=s.length-1;i>=0;i--)n=s[i],n._icon&&(n._setPos(e),n.setOpacity(0))},function(t){var i,n,s=t._childClusters;for(i=s.length-1;i>=0;i--)n=s[i],n._icon&&(n._setPos(e),n.setOpacity(0))})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(t,e,i){this._recursively(t,i,0,function(n){n._recursivelyAnimateChildrenIn(t,n._group._map.latLngToLayerPoint(n.getLatLng()).round(),e),n._isSingleParent()&&e-1===i?(n.setOpacity(1),n._recursivelyRemoveChildrenFromMap(t,e)):n.setOpacity(0),n._addToMap()})},_recursivelyBecomeVisible:function(t,e){this._recursively(t,0,e,null,function(t){t.setOpacity(1)})},_recursivelyAddChildrenToMap:function(t,e,i){this._recursively(i,-1,e,function(n){if(e!==n._zoom)for(var s=n._markers.length-1;s>=0;s--){var r=n._markers[s];i.contains(r._latlng)&&(t&&(r._backupLatlng=r.getLatLng(),r.setLatLng(t),r.setOpacity&&r.setOpacity(0)),n._group._featureGroup.addLayer(r))}},function(e){e._addToMap(t)})},_recursivelyRestoreChildPositions:function(t){for(var e=this._markers.length-1;e>=0;e--){var i=this._markers[e];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(t-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var s=this._childClusters.length-1;s>=0;s--)this._childClusters[s]._recursivelyRestoreChildPositions(t)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(t,e,i){var n,s;this._recursively(t,-1,e-1,function(t){for(s=t._markers.length-1;s>=0;s--)n=t._markers[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.setOpacity&&n.setOpacity(1))},function(t){for(s=t._childClusters.length-1;s>=0;s--)n=t._childClusters[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.setOpacity&&n.setOpacity(1))})},_recursively:function(t,e,i,n,s){var r,o,a=this._childClusters,h=this._zoom;if(e>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s);else if(n&&n(this),s&&this._zoom===i&&s(this),i>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s)},_recalculateBounds:function(){var t,e=this._markers,i=this._childClusters;for(this._bounds=new L.LatLngBounds,delete this._wLatLng,t=e.length-1;t>=0;t--)this._expandBounds(e[t]);for(t=i.length-1;t>=0;t--)this._expandBounds(i[t])},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}}),L.DistanceGrid=function(t){this._cellSize=t,this._sqCellSize=t*t,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(t,e){var i=this._getCoord(e.x),n=this._getCoord(e.y),s=this._grid,r=s[n]=s[n]||{},o=r[i]=r[i]||[],a=L.Util.stamp(t);this._objectPoint[a]=e,o.push(t)},updateObject:function(t,e){this.removeObject(t),this.addObject(t,e)},removeObject:function(t,e){var i,n,s=this._getCoord(e.x),r=this._getCoord(e.y),o=this._grid,a=o[r]=o[r]||{},h=a[s]=a[s]||[];for(delete this._objectPoint[L.Util.stamp(t)],i=0,n=h.length;n>i;i++)if(h[i]===t)return h.splice(i,1),1===n&&delete a[s],!0},eachObject:function(t,e){var i,n,s,r,o,a,h,_=this._grid;for(i in _){o=_[i];for(n in o)for(a=o[n],s=0,r=a.length;r>s;s++)h=t.call(e,a[s]),h&&(s--,r--)}},getNearObject:function(t){var e,i,n,s,r,o,a,h,_=this._getCoord(t.x),u=this._getCoord(t.y),l=this._objectPoint,d=this._sqCellSize,p=null;for(e=u-1;u+1>=e;e++)if(s=this._grid[e])for(i=_-1;_+1>=i;i++)if(r=s[i])for(n=0,o=r.length;o>n;n++)a=r[n],h=this._sqDist(l[L.Util.stamp(a)],t),d>h&&(d=h,p=a);return p},_getCoord:function(t){return Math.floor(t/this._cellSize)},_sqDist:function(t,e){var i=e.x-t.x,n=e.y-t.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(t,e){var i=e[1].lat-e[0].lat,n=e[0].lng-e[1].lng;return n*(t.lat-e[0].lat)+i*(t.lng-e[0].lng)},findMostDistantPointFromBaseLine:function(t,e){var i,n,s,r=0,o=null,a=[];for(i=e.length-1;i>=0;i--)n=e[i],s=this.getDistant(n,t),s>0&&(a.push(n),s>r&&(r=s,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(t,e){var i=[],n=this.findMostDistantPointFromBaseLine(t,e);return n.maxPoint?(i=i.concat(this.buildConvexHull([t[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,t[1]],n.newPoints))):[t[0]]},getConvexHull:function(t){var e,i=!1,n=!1,s=null,r=null;for(e=t.length-1;e>=0;e--){var o=t[e];(i===!1||o.lat>i)&&(s=o,i=o.lat),(n===!1||o.lat=0;e--)t=i[e].getLatLng(),n.push(t);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:Math.PI/6,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var t,e=this.getAllChildMarkers(),i=this._group,n=i._map,s=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,e.length>=this._circleSpiralSwitchover?t=this._generatePointsSpiral(e.length,s):(s.y+=10,t=this._generatePointsCircle(e.length,s)),this._animationSpiderfy(e,t)}},unspiderfy:function(t){this._group._inZoomAnimation||(this._animationUnspiderfy(t),this._group._spiderfied=null)},_generatePointsCircle:function(t,e){var i,n,s=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+t),r=s/this._2PI,o=this._2PI/t,a=[];for(a.length=t,i=t-1;i>=0;i--)n=this._circleStartAngle+i*o,a[i]=new L.Point(e.x+r*Math.cos(n),e.y+r*Math.sin(n))._round();return a},_generatePointsSpiral:function(t,e){var i,n=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthStart,s=this._group.options.spiderfyDistanceMultiplier*this._spiralFootSeparation,r=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthFactor,o=0,a=[];for(a.length=t,i=t-1;i>=0;i--)o+=s/n+5e-4*i,a[i]=new L.Point(e.x+n*Math.cos(o),e.y+n*Math.sin(o))._round(),n+=this._2PI*r/o;return a},_noanimationUnspiderfy:function(){var t,e,i=this._group,n=i._map,s=i._featureGroup,r=this.getAllChildMarkers();for(this.setOpacity(1),e=r.length-1;e>=0;e--)t=r[e],s.removeLayer(t),t._preSpiderfyLatlng&&(t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng),t.setZIndexOffset&&t.setZIndexOffset(0),t._spiderLeg&&(n.removeLayer(t._spiderLeg),delete t._spiderLeg);i._spiderfied=null}}),L.MarkerCluster.include(L.DomUtil.TRANSITION?{SVG_ANIMATION:function(){return e.createElementNS("http://www.w3.org/2000/svg","animate").toString().indexOf("SVGAnimate")>-1}(),_animationSpiderfy:function(t,i){var n,s,r,o,a=this,h=this._group,_=h._map,u=h._featureGroup,l=_.latLngToLayerPoint(this._latlng);for(n=t.length-1;n>=0;n--)s=t[n],s.setOpacity?(s.setZIndexOffset(1e6),s.setOpacity(0),u.addLayer(s),s._setPos(l)):u.addLayer(s);h._forceLayout(),h._animationStart();var d=L.Path.SVG?0:.3,p=L.Path.SVG_NS;for(n=t.length-1;n>=0;n--)if(o=_.layerPointToLatLng(i[n]),s=t[n],s._preSpiderfyLatlng=s._latlng,s.setLatLng(o),s.setOpacity&&s.setOpacity(1),r=new L.Polyline([a._latlng,o],{weight:1.5,color:"#222",opacity:d}),_.addLayer(r),s._spiderLeg=r,L.Path.SVG&&this.SVG_ANIMATION){var c=r._path.getTotalLength();r._path.setAttribute("stroke-dasharray",c+","+c);var m=e.createElementNS(p,"animate");m.setAttribute("attributeName","stroke-dashoffset"),m.setAttribute("begin","indefinite"),m.setAttribute("from",c),m.setAttribute("to",0),m.setAttribute("dur",.25),r._path.appendChild(m),m.beginElement(),m=e.createElementNS(p,"animate"),m.setAttribute("attributeName","stroke-opacity"),m.setAttribute("attributeName","stroke-opacity"),m.setAttribute("begin","indefinite"),m.setAttribute("from",0),m.setAttribute("to",.5),m.setAttribute("dur",.25),r._path.appendChild(m),m.beginElement()}if(a.setOpacity(.3),L.Path.SVG)for(this._group._forceLayout(),n=t.length-1;n>=0;n--)s=t[n]._spiderLeg,s.options.opacity=.5,s._path.setAttribute("stroke-opacity",.5);setTimeout(function(){h._animationEnd(),h.fire("spiderfied")},200)},_animationUnspiderfy:function(t){var e,i,n,s=this._group,r=s._map,o=s._featureGroup,a=t?r._latLngToNewLayerPoint(this._latlng,t.zoom,t.center):r.latLngToLayerPoint(this._latlng),h=this.getAllChildMarkers(),_=L.Path.SVG&&this.SVG_ANIMATION;for(s._animationStart(),this.setOpacity(1),i=h.length-1;i>=0;i--)e=h[i],e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng,e.setOpacity?(e._setPos(a),e.setOpacity(0)):o.removeLayer(e),_&&(n=e._spiderLeg._path.childNodes[0],n.setAttribute("to",n.getAttribute("from")),n.setAttribute("from",0),n.beginElement(),n=e._spiderLeg._path.childNodes[1],n.setAttribute("from",.5),n.setAttribute("to",0),n.setAttribute("stroke-opacity",0),n.beginElement(),e._spiderLeg._path.setAttribute("stroke-opacity",0)));setTimeout(function(){var t=0;for(i=h.length-1;i>=0;i--)e=h[i],e._spiderLeg&&t++;for(i=h.length-1;i>=0;i--)e=h[i],e._spiderLeg&&(e.setOpacity&&(e.setOpacity(1),e.setZIndexOffset(0)),t>1&&o.removeLayer(e),r.removeLayer(e._spiderLeg),delete e._spiderLeg);s._animationEnd()},200)}}:{_animationSpiderfy:function(t,e){var i,n,s,r,o=this._group,a=o._map,h=o._featureGroup;for(i=t.length-1;i>=0;i--)r=a.layerPointToLatLng(e[i]),n=t[i],n._preSpiderfyLatlng=n._latlng,n.setLatLng(r),n.setZIndexOffset&&n.setZIndexOffset(1e6),h.addLayer(n),s=new L.Polyline([this._latlng,r],{weight:1.5,color:"#222"}),a.addLayer(s),n._spiderLeg=s;this.setOpacity(.3),o.fire("spiderfied")},_animationUnspiderfy:function(){this._noanimationUnspiderfy()}}),L.MarkerClusterGroup.include({_spiderfied:null,_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Path.SVG&&!L.Browser.touch&&this._map._initPathRoot()},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy()},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(t){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(t))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(t){this._spiderfied&&this._spiderfied.unspiderfy(t)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(t){t._spiderLeg&&(this._featureGroup.removeLayer(t),t.setOpacity(1),t.setZIndexOffset(0),this._map.removeLayer(t._spiderLeg),delete t._spiderLeg)}})}(window,document);
diff --git a/views/shared/javascripts/leaflet/images/layers-2x.png b/views/shared/javascripts/leaflet/images/layers-2x.png
new file mode 100644
index 0000000..200c333
Binary files /dev/null and b/views/shared/javascripts/leaflet/images/layers-2x.png differ
diff --git a/views/shared/javascripts/leaflet/images/layers.png b/views/shared/javascripts/leaflet/images/layers.png
new file mode 100644
index 0000000..1a72e57
Binary files /dev/null and b/views/shared/javascripts/leaflet/images/layers.png differ
diff --git a/views/shared/javascripts/leaflet/images/marker-icon-2x.png b/views/shared/javascripts/leaflet/images/marker-icon-2x.png
new file mode 100644
index 0000000..88f9e50
Binary files /dev/null and b/views/shared/javascripts/leaflet/images/marker-icon-2x.png differ
diff --git a/views/shared/javascripts/leaflet/images/marker-icon.png b/views/shared/javascripts/leaflet/images/marker-icon.png
new file mode 100644
index 0000000..950edf2
Binary files /dev/null and b/views/shared/javascripts/leaflet/images/marker-icon.png differ
diff --git a/views/shared/javascripts/leaflet/images/marker-shadow.png b/views/shared/javascripts/leaflet/images/marker-shadow.png
new file mode 100644
index 0000000..9fd2979
Binary files /dev/null and b/views/shared/javascripts/leaflet/images/marker-shadow.png differ
diff --git a/views/shared/javascripts/leaflet/leaflet-providers.js b/views/shared/javascripts/leaflet/leaflet-providers.js
new file mode 100644
index 0000000..abb0c40
--- /dev/null
+++ b/views/shared/javascripts/leaflet/leaflet-providers.js
@@ -0,0 +1,1011 @@
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(['leaflet'], factory);
+ } else if (typeof modules === 'object' && module.exports) {
+ // define a Common JS module that relies on 'leaflet'
+ module.exports = factory(require('leaflet'));
+ } else {
+ // Assume Leaflet is loaded into global object L already
+ factory(L);
+ }
+}(this, function (L) {
+ 'use strict';
+
+ L.TileLayer.Provider = L.TileLayer.extend({
+ initialize: function (arg, options) {
+ var providers = L.TileLayer.Provider.providers;
+
+ var parts = arg.split('.');
+
+ var providerName = parts[0];
+ var variantName = parts[1];
+
+ if (!providers[providerName]) {
+ throw 'No such provider (' + providerName + ')';
+ }
+
+ var provider = {
+ url: providers[providerName].url,
+ options: providers[providerName].options
+ };
+
+ // overwrite values in provider from variant.
+ if (variantName && 'variants' in providers[providerName]) {
+ if (!(variantName in providers[providerName].variants)) {
+ throw 'No such variant of ' + providerName + ' (' + variantName + ')';
+ }
+ var variant = providers[providerName].variants[variantName];
+ var variantOptions;
+ if (typeof variant === 'string') {
+ variantOptions = {
+ variant: variant
+ };
+ } else {
+ variantOptions = variant.options;
+ }
+ provider = {
+ url: variant.url || provider.url,
+ options: L.Util.extend({}, provider.options, variantOptions)
+ };
+ }
+
+ // replace attribution placeholders with their values from toplevel provider attribution,
+ // recursively
+ var attributionReplacer = function (attr) {
+ if (attr.indexOf('{attribution.') === -1) {
+ return attr;
+ }
+ return attr.replace(/\{attribution.(\w*)\}/g,
+ function (match, attributionName) {
+ return attributionReplacer(providers[attributionName].options.attribution);
+ }
+ );
+ };
+ provider.options.attribution = attributionReplacer(provider.options.attribution);
+
+ // Compute final options combining provider options with any user overrides
+ var layerOpts = L.Util.extend({}, provider.options, options);
+ L.TileLayer.prototype.initialize.call(this, provider.url, layerOpts);
+ }
+ });
+
+ /**
+ * Definition of providers.
+ * see http://leafletjs.com/reference.html#tilelayer for options in the options map.
+ */
+
+ L.TileLayer.Provider.providers = {
+ OpenStreetMap: {
+ url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 19,
+ attribution:
+ '© OpenStreetMap contributors'
+ },
+ variants: {
+ Mapnik: {},
+ DE: {
+ url: 'https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 18
+ }
+ },
+ CH: {
+ url: 'https://tile.osm.ch/switzerland/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 18,
+ bounds: [[45, 5], [48, 11]]
+ }
+ },
+ France: {
+ url: 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 20,
+ attribution: '© Openstreetmap France | {attribution.OpenStreetMap}'
+ }
+ },
+ HOT: {
+ url: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
+ options: {
+ attribution:
+ '{attribution.OpenStreetMap}, ' +
+ 'Tiles style by Humanitarian OpenStreetMap Team ' +
+ 'hosted by OpenStreetMap France '
+ }
+ },
+ BZH: {
+ url: 'https://tile.openstreetmap.bzh/br/{z}/{x}/{y}.png',
+ options: {
+ attribution: '{attribution.OpenStreetMap}, Tiles courtesy of Breton OpenStreetMap Team ',
+ bounds: [[46.2, -5.5], [50, 0.7]]
+ }
+ }
+ }
+ },
+ OpenSeaMap: {
+ url: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
+ options: {
+ attribution: 'Map data: © OpenSeaMap contributors'
+ }
+ },
+ OpenPtMap: {
+ url: 'http://openptmap.org/tiles/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 17,
+ attribution: 'Map data: © OpenPtMap contributors'
+ }
+ },
+ OpenTopoMap: {
+ url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 17,
+ attribution: 'Map data: {attribution.OpenStreetMap}, SRTM | Map style: © OpenTopoMap (CC-BY-SA )'
+ }
+ },
+ OpenRailwayMap: {
+ url: 'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 19,
+ attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenRailwayMap (CC-BY-SA )'
+ }
+ },
+ OpenFireMap: {
+ url: 'http://openfiremap.org/hytiles/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 19,
+ attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenFireMap (CC-BY-SA )'
+ }
+ },
+ SafeCast: {
+ url: 'https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 16,
+ attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © SafeCast (CC-BY-SA )'
+ }
+ },
+ Stadia: {
+ url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png',
+ options: {
+ maxZoom: 20,
+ attribution: '© Stadia Maps , © OpenMapTiles © OpenStreetMap contributors'
+ },
+ variants: {
+ AlidadeSmooth: {
+ url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png'
+ },
+ AlidadeSmoothDark: {
+ url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png'
+ },
+ OSMBright: {
+ url: 'https://tiles.stadiamaps.com/tiles/osm_bright/{z}/{x}/{y}{r}.png'
+ },
+ Outdoors: {
+ url: 'https://tiles.stadiamaps.com/tiles/outdoors/{z}/{x}/{y}{r}.png'
+ }
+ }
+ },
+ Thunderforest: {
+ url: 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
+ options: {
+ attribution:
+ '© Thunderforest , {attribution.OpenStreetMap}',
+ variant: 'cycle',
+ apikey: '',
+ maxZoom: 22
+ },
+ variants: {
+ OpenCycleMap: 'cycle',
+ Transport: {
+ options: {
+ variant: 'transport'
+ }
+ },
+ TransportDark: {
+ options: {
+ variant: 'transport-dark'
+ }
+ },
+ SpinalMap: {
+ options: {
+ variant: 'spinal-map'
+ }
+ },
+ Landscape: 'landscape',
+ Outdoors: 'outdoors',
+ Pioneer: 'pioneer',
+ MobileAtlas: 'mobile-atlas',
+ Neighbourhood: 'neighbourhood'
+ }
+ },
+ CyclOSM: {
+ url: 'https://dev.{s}.tile.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 20,
+ attribution: 'CyclOSM | Map data: {attribution.OpenStreetMap}'
+ }
+ },
+ Hydda: {
+ url: 'https://{s}.tile.openstreetmap.se/hydda/{variant}/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 20,
+ variant: 'full',
+ attribution: 'Tiles courtesy of OpenStreetMap Sweden — Map data {attribution.OpenStreetMap}'
+ },
+ variants: {
+ Full: 'full',
+ Base: 'base',
+ RoadsAndLabels: 'roads_and_labels'
+ }
+ },
+ Jawg: {
+ url: 'https://{s}.tile.jawg.io/{variant}/{z}/{x}/{y}{r}.png?access-token={accessToken}',
+ options: {
+ attribution:
+ '© Jawg Maps ' +
+ '{attribution.OpenStreetMap}',
+ minZoom: 0,
+ maxZoom: 22,
+ subdomains: 'abcd',
+ variant: 'jawg-terrain',
+ // Get your own Jawg access token here : https://www.jawg.io/lab/
+ // NB : this is a demonstration key that comes with no guarantee
+ accessToken: '',
+ },
+ variants: {
+ Streets: 'jawg-streets',
+ Terrain: 'jawg-terrain',
+ Sunny: 'jawg-sunny',
+ Dark: 'jawg-dark',
+ Light: 'jawg-light',
+ Matrix: 'jawg-matrix'
+ }
+ },
+ MapBox: {
+ url: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}{r}?access_token={accessToken}',
+ options: {
+ attribution:
+ '© Mapbox ' +
+ '{attribution.OpenStreetMap} ' +
+ 'Improve this map ',
+ tileSize: 512,
+ maxZoom: 18,
+ zoomOffset: -1,
+ id: 'mapbox/streets-v11',
+ accessToken: '',
+ }
+ },
+ MapTiler: {
+ url: 'https://api.maptiler.com/maps/{variant}/{z}/{x}/{y}{r}.{ext}?key={key}',
+ options: {
+ attribution:
+ '© MapTiler © OpenStreetMap contributors ',
+ variant: 'streets',
+ ext: 'png',
+ key: '',
+ tileSize: 512,
+ zoomOffset: -1,
+ minZoom: 0,
+ maxZoom: 21
+ },
+ variants: {
+ Streets: 'streets',
+ Basic: 'basic',
+ Bright: 'bright',
+ Pastel: 'pastel',
+ Positron: 'positron',
+ Hybrid: {
+ options: {
+ variant: 'hybrid',
+ ext: 'jpg'
+ }
+ },
+ Toner: 'toner',
+ Topo: 'topo',
+ Voyager: 'voyager'
+ }
+ },
+ Stamen: {
+ url: 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
+ options: {
+ attribution:
+ 'Map tiles by Stamen Design , ' +
+ 'CC BY 3.0 — ' +
+ 'Map data {attribution.OpenStreetMap}',
+ subdomains: 'abcd',
+ minZoom: 0,
+ maxZoom: 20,
+ variant: 'toner',
+ ext: 'png'
+ },
+ variants: {
+ Toner: 'toner',
+ TonerBackground: 'toner-background',
+ TonerHybrid: 'toner-hybrid',
+ TonerLines: 'toner-lines',
+ TonerLabels: 'toner-labels',
+ TonerLite: 'toner-lite',
+ Watercolor: {
+ url: 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}.{ext}',
+ options: {
+ variant: 'watercolor',
+ ext: 'jpg',
+ minZoom: 1,
+ maxZoom: 16
+ }
+ },
+ Terrain: {
+ options: {
+ variant: 'terrain',
+ minZoom: 0,
+ maxZoom: 18
+ }
+ },
+ TerrainBackground: {
+ options: {
+ variant: 'terrain-background',
+ minZoom: 0,
+ maxZoom: 18
+ }
+ },
+ TerrainLabels: {
+ options: {
+ variant: 'terrain-labels',
+ minZoom: 0,
+ maxZoom: 18
+ }
+ },
+ TopOSMRelief: {
+ url: 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}.{ext}',
+ options: {
+ variant: 'toposm-color-relief',
+ ext: 'jpg',
+ bounds: [[22, -132], [51, -56]]
+ }
+ },
+ TopOSMFeatures: {
+ options: {
+ variant: 'toposm-features',
+ bounds: [[22, -132], [51, -56]],
+ opacity: 0.9
+ }
+ }
+ }
+ },
+ TomTom: {
+ url: 'https://{s}.api.tomtom.com/map/1/tile/{variant}/{style}/{z}/{x}/{y}.{ext}?key={apikey}',
+ options: {
+ variant: 'basic',
+ maxZoom: 22,
+ attribution:
+ '© 1992 - ' + new Date().getFullYear() + ' TomTom. ',
+ subdomains: 'abcd',
+ style: 'main',
+ ext: 'png',
+ apikey: '',
+ },
+ variants: {
+ Basic: 'basic',
+ Hybrid: 'hybrid',
+ Labels: 'labels'
+ }
+ },
+ Esri: {
+ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
+ options: {
+ variant: 'World_Street_Map',
+ attribution: 'Tiles © Esri'
+ },
+ variants: {
+ WorldStreetMap: {
+ options: {
+ attribution:
+ '{attribution.Esri} — ' +
+ 'Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012'
+ }
+ },
+ DeLorme: {
+ options: {
+ variant: 'Specialty/DeLorme_World_Base_Map',
+ minZoom: 1,
+ maxZoom: 11,
+ attribution: '{attribution.Esri} — Copyright: ©2012 DeLorme'
+ }
+ },
+ WorldTopoMap: {
+ options: {
+ variant: 'World_Topo_Map',
+ attribution:
+ '{attribution.Esri} — ' +
+ 'Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community'
+ }
+ },
+ WorldImagery: {
+ options: {
+ variant: 'World_Imagery',
+ attribution:
+ '{attribution.Esri} — ' +
+ 'Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
+ }
+ },
+ WorldTerrain: {
+ options: {
+ variant: 'World_Terrain_Base',
+ maxZoom: 13,
+ attribution:
+ '{attribution.Esri} — ' +
+ 'Source: USGS, Esri, TANA, DeLorme, and NPS'
+ }
+ },
+ WorldShadedRelief: {
+ options: {
+ variant: 'World_Shaded_Relief',
+ maxZoom: 13,
+ attribution: '{attribution.Esri} — Source: Esri'
+ }
+ },
+ WorldPhysical: {
+ options: {
+ variant: 'World_Physical_Map',
+ maxZoom: 8,
+ attribution: '{attribution.Esri} — Source: US National Park Service'
+ }
+ },
+ OceanBasemap: {
+ options: {
+ variant: 'Ocean_Basemap',
+ maxZoom: 13,
+ attribution: '{attribution.Esri} — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri'
+ }
+ },
+ NatGeoWorldMap: {
+ options: {
+ variant: 'NatGeo_World_Map',
+ maxZoom: 16,
+ attribution: '{attribution.Esri} — National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC'
+ }
+ },
+ WorldGrayCanvas: {
+ options: {
+ variant: 'Canvas/World_Light_Gray_Base',
+ maxZoom: 16,
+ attribution: '{attribution.Esri} — Esri, DeLorme, NAVTEQ'
+ }
+ }
+ }
+ },
+ OpenWeatherMap: {
+ url: 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
+ options: {
+ maxZoom: 19,
+ attribution: 'Map data © OpenWeatherMap ',
+ apiKey:'',
+ opacity: 0.5
+ },
+ variants: {
+ Clouds: 'clouds',
+ CloudsClassic: 'clouds_cls',
+ Precipitation: 'precipitation',
+ PrecipitationClassic: 'precipitation_cls',
+ Rain: 'rain',
+ RainClassic: 'rain_cls',
+ Pressure: 'pressure',
+ PressureContour: 'pressure_cntr',
+ Wind: 'wind',
+ Temperature: 'temp',
+ Snow: 'snow'
+ }
+ },
+ HERE: {
+ /*
+ * HERE maps, formerly Nokia maps.
+ * These basemaps are free, but you need an api id and app key. Please sign up at
+ * https://developer.here.com/plans
+ */
+ url:
+ 'https://{s}.{base}.maps.api.here.com/maptile/2.1/' +
+ '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' +
+ 'app_id={app_id}&app_code={app_code}&lg={language}',
+ options: {
+ attribution:
+ 'Map © 1987-' + new Date().getFullYear() + ' HERE ',
+ subdomains: '1234',
+ mapID: 'newest',
+ 'app_id': '',
+ 'app_code': '',
+ base: 'base',
+ variant: 'normal.day',
+ maxZoom: 20,
+ type: 'maptile',
+ language: 'eng',
+ format: 'png8',
+ size: '256'
+ },
+ variants: {
+ normalDay: 'normal.day',
+ normalDayCustom: 'normal.day.custom',
+ normalDayGrey: 'normal.day.grey',
+ normalDayMobile: 'normal.day.mobile',
+ normalDayGreyMobile: 'normal.day.grey.mobile',
+ normalDayTransit: 'normal.day.transit',
+ normalDayTransitMobile: 'normal.day.transit.mobile',
+ normalDayTraffic: {
+ options: {
+ variant: 'normal.traffic.day',
+ base: 'traffic',
+ type: 'traffictile'
+ }
+ },
+ normalNight: 'normal.night',
+ normalNightMobile: 'normal.night.mobile',
+ normalNightGrey: 'normal.night.grey',
+ normalNightGreyMobile: 'normal.night.grey.mobile',
+ normalNightTransit: 'normal.night.transit',
+ normalNightTransitMobile: 'normal.night.transit.mobile',
+ reducedDay: 'reduced.day',
+ reducedNight: 'reduced.night',
+ basicMap: {
+ options: {
+ type: 'basetile'
+ }
+ },
+ mapLabels: {
+ options: {
+ type: 'labeltile',
+ format: 'png'
+ }
+ },
+ trafficFlow: {
+ options: {
+ base: 'traffic',
+ type: 'flowtile'
+ }
+ },
+ carnavDayGrey: 'carnav.day.grey',
+ hybridDay: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day'
+ }
+ },
+ hybridDayMobile: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day.mobile'
+ }
+ },
+ hybridDayTransit: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day.transit'
+ }
+ },
+ hybridDayGrey: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.grey.day'
+ }
+ },
+ hybridDayTraffic: {
+ options: {
+ variant: 'hybrid.traffic.day',
+ base: 'traffic',
+ type: 'traffictile'
+ }
+ },
+ pedestrianDay: 'pedestrian.day',
+ pedestrianNight: 'pedestrian.night',
+ satelliteDay: {
+ options: {
+ base: 'aerial',
+ variant: 'satellite.day'
+ }
+ },
+ terrainDay: {
+ options: {
+ base: 'aerial',
+ variant: 'terrain.day'
+ }
+ },
+ terrainDayMobile: {
+ options: {
+ base: 'aerial',
+ variant: 'terrain.day.mobile'
+ }
+ }
+ }
+ },
+ HEREv3: {
+ /*
+ * HERE maps API Version 3.
+ * These basemaps are free, but you need an API key. Please sign up at
+ * https://developer.here.com/plans
+ * Version 3 deprecates the app_id and app_code access in favor of apiKey
+ *
+ * Supported access methods as of 2019/12/21:
+ * @see https://developer.here.com/faqs#access-control-1--how-do-you-control-access-to-here-location-services
+ */
+ url:
+ 'https://{s}.{base}.maps.ls.hereapi.com/maptile/2.1/' +
+ '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' +
+ 'apiKey={apiKey}&lg={language}',
+ options: {
+ attribution:
+ 'Map © 1987-' + new Date().getFullYear() + ' HERE ',
+ subdomains: '1234',
+ mapID: 'newest',
+ apiKey: '',
+ base: 'base',
+ variant: 'normal.day',
+ maxZoom: 20,
+ type: 'maptile',
+ language: 'eng',
+ format: 'png8',
+ size: '256'
+ },
+ variants: {
+ normalDay: 'normal.day',
+ normalDayCustom: 'normal.day.custom',
+ normalDayGrey: 'normal.day.grey',
+ normalDayMobile: 'normal.day.mobile',
+ normalDayGreyMobile: 'normal.day.grey.mobile',
+ normalDayTransit: 'normal.day.transit',
+ normalDayTransitMobile: 'normal.day.transit.mobile',
+ normalNight: 'normal.night',
+ normalNightMobile: 'normal.night.mobile',
+ normalNightGrey: 'normal.night.grey',
+ normalNightGreyMobile: 'normal.night.grey.mobile',
+ normalNightTransit: 'normal.night.transit',
+ normalNightTransitMobile: 'normal.night.transit.mobile',
+ reducedDay: 'reduced.day',
+ reducedNight: 'reduced.night',
+ basicMap: {
+ options: {
+ type: 'basetile'
+ }
+ },
+ mapLabels: {
+ options: {
+ type: 'labeltile',
+ format: 'png'
+ }
+ },
+ trafficFlow: {
+ options: {
+ base: 'traffic',
+ type: 'flowtile'
+ }
+ },
+ carnavDayGrey: 'carnav.day.grey',
+ hybridDay: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day'
+ }
+ },
+ hybridDayMobile: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day.mobile'
+ }
+ },
+ hybridDayTransit: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day.transit'
+ }
+ },
+ hybridDayGrey: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.grey.day'
+ }
+ },
+ pedestrianDay: 'pedestrian.day',
+ pedestrianNight: 'pedestrian.night',
+ satelliteDay: {
+ options: {
+ base: 'aerial',
+ variant: 'satellite.day'
+ }
+ },
+ terrainDay: {
+ options: {
+ base: 'aerial',
+ variant: 'terrain.day'
+ }
+ },
+ terrainDayMobile: {
+ options: {
+ base: 'aerial',
+ variant: 'terrain.day.mobile'
+ }
+ }
+ }
+ },
+ FreeMapSK: {
+ url: 'http://t{s}.freemap.sk/T/{z}/{x}/{y}.jpeg',
+ options: {
+ minZoom: 8,
+ maxZoom: 16,
+ subdomains: '1234',
+ bounds: [[47.204642, 15.996093], [49.830896, 22.576904]],
+ attribution:
+ '{attribution.OpenStreetMap}, vizualization CC-By-SA 2.0 Freemap.sk '
+ }
+ },
+ MtbMap: {
+ url: 'http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png',
+ options: {
+ attribution:
+ '{attribution.OpenStreetMap} & USGS'
+ }
+ },
+ CartoDB: {
+ url: 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
+ options: {
+ attribution: '{attribution.OpenStreetMap} © CARTO ',
+ subdomains: 'abcd',
+ maxZoom: 19,
+ variant: 'light_all'
+ },
+ variants: {
+ Positron: 'light_all',
+ PositronNoLabels: 'light_nolabels',
+ PositronOnlyLabels: 'light_only_labels',
+ DarkMatter: 'dark_all',
+ DarkMatterNoLabels: 'dark_nolabels',
+ DarkMatterOnlyLabels: 'dark_only_labels',
+ Voyager: 'rastertiles/voyager',
+ VoyagerNoLabels: 'rastertiles/voyager_nolabels',
+ VoyagerOnlyLabels: 'rastertiles/voyager_only_labels',
+ VoyagerLabelsUnder: 'rastertiles/voyager_labels_under'
+ }
+ },
+ HikeBike: {
+ url: 'https://tiles.wmflabs.org/{variant}/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 19,
+ attribution: '{attribution.OpenStreetMap}',
+ variant: 'hikebike'
+ },
+ variants: {
+ HikeBike: {},
+ HillShading: {
+ options: {
+ maxZoom: 15,
+ variant: 'hillshading'
+ }
+ }
+ }
+ },
+ BasemapAT: {
+ url: 'https://maps{s}.wien.gv.at/basemap/{variant}/{type}/google3857/{z}/{y}/{x}.{format}',
+ options: {
+ maxZoom: 19,
+ attribution: 'Datenquelle: basemap.at ',
+ subdomains: ['', '1', '2', '3', '4'],
+ type: 'normal',
+ format: 'png',
+ bounds: [[46.358770, 8.782379], [49.037872, 17.189532]],
+ variant: 'geolandbasemap'
+ },
+ variants: {
+ basemap: {
+ options: {
+ maxZoom: 20, // currently only in Vienna
+ variant: 'geolandbasemap'
+ }
+ },
+ grau: 'bmapgrau',
+ overlay: 'bmapoverlay',
+ terrain: {
+ options: {
+ variant: 'bmapgelaende',
+ type: 'grau',
+ format: 'jpeg'
+ }
+ },
+ surface: {
+ options: {
+ variant: 'bmapoberflaeche',
+ type: 'grau',
+ format: 'jpeg'
+ }
+ },
+ highdpi: {
+ options: {
+ variant: 'bmaphidpi',
+ format: 'jpeg'
+ }
+ },
+ orthofoto: {
+ options: {
+ maxZoom: 20, // currently only in Vienna
+ variant: 'bmaporthofoto30cm',
+ format: 'jpeg'
+ }
+ }
+ }
+ },
+ nlmaps: {
+ url: 'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/{variant}/EPSG:3857/{z}/{x}/{y}.png',
+ options: {
+ minZoom: 6,
+ maxZoom: 19,
+ bounds: [[50.5, 3.25], [54, 7.6]],
+ attribution: 'Kaartgegevens © Kadaster '
+ },
+ variants: {
+ 'standaard': 'brtachtergrondkaart',
+ 'pastel': 'brtachtergrondkaartpastel',
+ 'grijs': 'brtachtergrondkaartgrijs',
+ 'luchtfoto': {
+ 'url': 'https://geodata.nationaalgeoregister.nl/luchtfoto/rgb/wmts/2018_ortho25/EPSG:3857/{z}/{x}/{y}.png',
+ }
+ }
+ },
+ NASAGIBS: {
+ url: 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{maxZoom}/{z}/{y}/{x}.{format}',
+ options: {
+ attribution:
+ 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System ' +
+ '(ESDIS ) with funding provided by NASA/HQ.',
+ bounds: [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
+ minZoom: 1,
+ maxZoom: 9,
+ format: 'jpg',
+ time: '',
+ tilematrixset: 'GoogleMapsCompatible_Level'
+ },
+ variants: {
+ ModisTerraTrueColorCR: 'MODIS_Terra_CorrectedReflectance_TrueColor',
+ ModisTerraBands367CR: 'MODIS_Terra_CorrectedReflectance_Bands367',
+ ViirsEarthAtNight2012: {
+ options: {
+ variant: 'VIIRS_CityLights_2012',
+ maxZoom: 8
+ }
+ },
+ ModisTerraLSTDay: {
+ options: {
+ variant: 'MODIS_Terra_Land_Surface_Temp_Day',
+ format: 'png',
+ maxZoom: 7,
+ opacity: 0.75
+ }
+ },
+ ModisTerraSnowCover: {
+ options: {
+ variant: 'MODIS_Terra_Snow_Cover',
+ format: 'png',
+ maxZoom: 8,
+ opacity: 0.75
+ }
+ },
+ ModisTerraAOD: {
+ options: {
+ variant: 'MODIS_Terra_Aerosol',
+ format: 'png',
+ maxZoom: 6,
+ opacity: 0.75
+ }
+ },
+ ModisTerraChlorophyll: {
+ options: {
+ variant: 'MODIS_Terra_Chlorophyll_A',
+ format: 'png',
+ maxZoom: 7,
+ opacity: 0.75
+ }
+ }
+ }
+ },
+ NLS: {
+ // NLS maps are copyright National library of Scotland.
+ // http://maps.nls.uk/projects/api/index.html
+ // Please contact NLS for anything other than non-commercial low volume usage
+ //
+ // Map sources: Ordnance Survey 1:1m to 1:63K, 1920s-1940s
+ // z0-9 - 1:1m
+ // z10-11 - quarter inch (1:253440)
+ // z12-18 - one inch (1:63360)
+ url: 'https://nls-{s}.tileserver.com/nls/{z}/{x}/{y}.jpg',
+ options: {
+ attribution: 'National Library of Scotland Historic Maps ',
+ bounds: [[49.6, -12], [61.7, 3]],
+ minZoom: 1,
+ maxZoom: 18,
+ subdomains: '0123',
+ }
+ },
+ JusticeMap: {
+ // Justice Map (http://www.justicemap.org/)
+ // Visualize race and income data for your community, county and country.
+ // Includes tools for data journalists, bloggers and community activists.
+ url: 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
+ options: {
+ attribution: 'Justice Map ',
+ // one of 'county', 'tract', 'block'
+ size: 'county',
+ // Bounds for USA, including Alaska and Hawaii
+ bounds: [[14, -180], [72, -56]]
+ },
+ variants: {
+ income: 'income',
+ americanIndian: 'indian',
+ asian: 'asian',
+ black: 'black',
+ hispanic: 'hispanic',
+ multi: 'multi',
+ nonWhite: 'nonwhite',
+ white: 'white',
+ plurality: 'plural'
+ }
+ },
+ Wikimedia: {
+ url: 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}{r}.png',
+ options: {
+ attribution: 'Wikimedia ',
+ minZoom: 1,
+ maxZoom: 19
+ }
+ },
+ GeoportailFrance: {
+ url: 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}',
+ options: {
+ attribution: 'Geoportail France ',
+ bounds: [[-75, -180], [81, 180]],
+ minZoom: 2,
+ maxZoom: 18,
+ // Get your own geoportail apikey here : http://professionnels.ign.fr/ign/contrats/
+ // NB : 'choisirgeoportail' is a demonstration key that comes with no guarantee
+ apikey: 'choisirgeoportail',
+ format: 'image/jpeg',
+ style : 'normal',
+ variant: 'GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD'
+ },
+ variants: {
+ parcels: {
+ options : {
+ variant: 'CADASTRALPARCELS.PARCELS',
+ maxZoom: 20,
+ style : 'bdparcellaire',
+ format: 'image/png'
+ }
+ },
+ ignMaps: 'GEOGRAPHICALGRIDSYSTEMS.MAPS',
+ maps: 'GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD',
+ orthos: {
+ options: {
+ maxZoom: 19,
+ variant: 'ORTHOIMAGERY.ORTHOPHOTOS'
+ }
+ }
+ }
+ },
+ OneMapSG: {
+ url: 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
+ options: {
+ variant: 'Default',
+ minZoom: 11,
+ maxZoom: 18,
+ bounds: [[1.56073, 104.11475], [1.16, 103.502]],
+ attribution: ' New OneMap | Map data © contributors, Singapore Land Authority '
+ },
+ variants: {
+ Default: 'Default',
+ Night: 'Night',
+ Original: 'Original',
+ Grey: 'Grey',
+ LandLot: 'LandLot'
+ }
+ }
+ };
+
+ L.tileLayer.provider = function (provider, options) {
+ return new L.TileLayer.Provider(provider, options);
+ };
+
+ return L;
+}));
diff --git a/views/shared/javascripts/leaflet/leaflet-src.esm.js b/views/shared/javascripts/leaflet/leaflet-src.esm.js
new file mode 100644
index 0000000..fb08729
--- /dev/null
+++ b/views/shared/javascripts/leaflet/leaflet-src.esm.js
@@ -0,0 +1,13986 @@
+/* @preserve
+ * Leaflet 1.6.0+Detached: 0c81bdf904d864fd12a286e3d1979f47aba17991.0c81bdf, a JS library for interactive maps. http://leafletjs.com
+ * (c) 2010-2019 Vladimir Agafonkin, (c) 2010-2011 CloudMade
+ */
+
+var version = "1.6.0+HEAD.0c81bdf";
+
+/*
+ * @namespace Util
+ *
+ * Various utility functions, used by Leaflet internally.
+ */
+
+var freeze = Object.freeze;
+Object.freeze = function (obj) { return obj; };
+
+// @function extend(dest: Object, src?: Object): Object
+// Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut.
+function extend(dest) {
+ var i, j, len, src;
+
+ for (j = 1, len = arguments.length; j < len; j++) {
+ src = arguments[j];
+ for (i in src) {
+ dest[i] = src[i];
+ }
+ }
+ return dest;
+}
+
+// @function create(proto: Object, properties?: Object): Object
+// Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create)
+var create = Object.create || (function () {
+ function F() {}
+ return function (proto) {
+ F.prototype = proto;
+ return new F();
+ };
+})();
+
+// @function bind(fn: Function, …): Function
+// Returns a new function bound to the arguments passed, like [Function.prototype.bind](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind).
+// Has a `L.bind()` shortcut.
+function bind(fn, obj) {
+ var slice = Array.prototype.slice;
+
+ if (fn.bind) {
+ return fn.bind.apply(fn, slice.call(arguments, 1));
+ }
+
+ var args = slice.call(arguments, 2);
+
+ return function () {
+ return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments);
+ };
+}
+
+// @property lastId: Number
+// Last unique ID used by [`stamp()`](#util-stamp)
+var lastId = 0;
+
+// @function stamp(obj: Object): Number
+// Returns the unique ID of an object, assigning it one if it doesn't have it.
+function stamp(obj) {
+ /*eslint-disable */
+ obj._leaflet_id = obj._leaflet_id || ++lastId;
+ return obj._leaflet_id;
+ /* eslint-enable */
+}
+
+// @function throttle(fn: Function, time: Number, context: Object): Function
+// Returns a function which executes function `fn` with the given scope `context`
+// (so that the `this` keyword refers to `context` inside `fn`'s code). The function
+// `fn` will be called no more than one time per given amount of `time`. The arguments
+// received by the bound function will be any arguments passed when binding the
+// function, followed by any arguments passed when invoking the bound function.
+// Has an `L.throttle` shortcut.
+function throttle(fn, time, context) {
+ var lock, args, wrapperFn, later;
+
+ later = function () {
+ // reset lock and call if queued
+ lock = false;
+ if (args) {
+ wrapperFn.apply(context, args);
+ args = false;
+ }
+ };
+
+ wrapperFn = function () {
+ if (lock) {
+ // called too soon, queue to call later
+ args = arguments;
+
+ } else {
+ // call and lock until later
+ fn.apply(context, arguments);
+ setTimeout(later, time);
+ lock = true;
+ }
+ };
+
+ return wrapperFn;
+}
+
+// @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number
+// Returns the number `num` modulo `range` in such a way so it lies within
+// `range[0]` and `range[1]`. The returned value will be always smaller than
+// `range[1]` unless `includeMax` is set to `true`.
+function wrapNum(x, range, includeMax) {
+ var max = range[1],
+ min = range[0],
+ d = max - min;
+ return x === max && includeMax ? x : ((x - min) % d + d) % d + min;
+}
+
+// @function falseFn(): Function
+// Returns a function which always returns `false`.
+function falseFn() { return false; }
+
+// @function formatNum(num: Number, digits?: Number): Number
+// Returns the number `num` rounded to `digits` decimals, or to 6 decimals by default.
+function formatNum(num, digits) {
+ var pow = Math.pow(10, (digits === undefined ? 6 : digits));
+ return Math.round(num * pow) / pow;
+}
+
+// @function trim(str: String): String
+// Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim)
+function trim(str) {
+ return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
+}
+
+// @function splitWords(str: String): String[]
+// Trims and splits the string on whitespace and returns the array of parts.
+function splitWords(str) {
+ return trim(str).split(/\s+/);
+}
+
+// @function setOptions(obj: Object, options: Object): Object
+// Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut.
+function setOptions(obj, options) {
+ if (!obj.hasOwnProperty('options')) {
+ obj.options = obj.options ? create(obj.options) : {};
+ }
+ for (var i in options) {
+ obj.options[i] = options[i];
+ }
+ return obj.options;
+}
+
+// @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String
+// Converts an object into a parameter URL string, e.g. `{a: "foo", b: "bar"}`
+// translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will
+// be appended at the end. If `uppercase` is `true`, the parameter names will
+// be uppercased (e.g. `'?A=foo&B=bar'`)
+function getParamString(obj, existingUrl, uppercase) {
+ var params = [];
+ for (var i in obj) {
+ params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i]));
+ }
+ return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&');
+}
+
+var templateRe = /\{ *([\w_-]+) *\}/g;
+
+// @function template(str: String, data: Object): String
+// Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'`
+// and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string
+// `('Hello foo, bar')`. You can also specify functions instead of strings for
+// data values — they will be evaluated passing `data` as an argument.
+function template(str, data) {
+ return str.replace(templateRe, function (str, key) {
+ var value = data[key];
+
+ if (value === undefined) {
+ throw new Error('No value provided for variable ' + str);
+
+ } else if (typeof value === 'function') {
+ value = value(data);
+ }
+ return value;
+ });
+}
+
+// @function isArray(obj): Boolean
+// Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray)
+var isArray = Array.isArray || function (obj) {
+ return (Object.prototype.toString.call(obj) === '[object Array]');
+};
+
+// @function indexOf(array: Array, el: Object): Number
+// Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf)
+function indexOf(array, el) {
+ for (var i = 0; i < array.length; i++) {
+ if (array[i] === el) { return i; }
+ }
+ return -1;
+}
+
+// @property emptyImageUrl: String
+// Data URI string containing a base64-encoded empty GIF image.
+// Used as a hack to free memory from unused images on WebKit-powered
+// mobile devices (by setting image `src` to this string).
+var emptyImageUrl = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
+
+// inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/
+
+function getPrefixed(name) {
+ return window['webkit' + name] || window['moz' + name] || window['ms' + name];
+}
+
+var lastTime = 0;
+
+// fallback for IE 7-8
+function timeoutDefer(fn) {
+ var time = +new Date(),
+ timeToCall = Math.max(0, 16 - (time - lastTime));
+
+ lastTime = time + timeToCall;
+ return window.setTimeout(fn, timeToCall);
+}
+
+var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer;
+var cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') ||
+ getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); };
+
+// @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number
+// Schedules `fn` to be executed when the browser repaints. `fn` is bound to
+// `context` if given. When `immediate` is set, `fn` is called immediately if
+// the browser doesn't have native support for
+// [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame),
+// otherwise it's delayed. Returns a request ID that can be used to cancel the request.
+function requestAnimFrame(fn, context, immediate) {
+ if (immediate && requestFn === timeoutDefer) {
+ fn.call(context);
+ } else {
+ return requestFn.call(window, bind(fn, context));
+ }
+}
+
+// @function cancelAnimFrame(id: Number): undefined
+// Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame).
+function cancelAnimFrame(id) {
+ if (id) {
+ cancelFn.call(window, id);
+ }
+}
+
+
+var Util = (Object.freeze || Object)({
+ freeze: freeze,
+ extend: extend,
+ create: create,
+ bind: bind,
+ lastId: lastId,
+ stamp: stamp,
+ throttle: throttle,
+ wrapNum: wrapNum,
+ falseFn: falseFn,
+ formatNum: formatNum,
+ trim: trim,
+ splitWords: splitWords,
+ setOptions: setOptions,
+ getParamString: getParamString,
+ template: template,
+ isArray: isArray,
+ indexOf: indexOf,
+ emptyImageUrl: emptyImageUrl,
+ requestFn: requestFn,
+ cancelFn: cancelFn,
+ requestAnimFrame: requestAnimFrame,
+ cancelAnimFrame: cancelAnimFrame
+});
+
+// @class Class
+// @aka L.Class
+
+// @section
+// @uninheritable
+
+// Thanks to John Resig and Dean Edwards for inspiration!
+
+function Class() {}
+
+Class.extend = function (props) {
+
+ // @function extend(props: Object): Function
+ // [Extends the current class](#class-inheritance) given the properties to be included.
+ // Returns a Javascript function that is a class constructor (to be called with `new`).
+ var NewClass = function () {
+
+ // call the constructor
+ if (this.initialize) {
+ this.initialize.apply(this, arguments);
+ }
+
+ // call all constructor hooks
+ this.callInitHooks();
+ };
+
+ var parentProto = NewClass.__super__ = this.prototype;
+
+ var proto = create(parentProto);
+ proto.constructor = NewClass;
+
+ NewClass.prototype = proto;
+
+ // inherit parent's statics
+ for (var i in this) {
+ if (this.hasOwnProperty(i) && i !== 'prototype' && i !== '__super__') {
+ NewClass[i] = this[i];
+ }
+ }
+
+ // mix static properties into the class
+ if (props.statics) {
+ extend(NewClass, props.statics);
+ delete props.statics;
+ }
+
+ // mix includes into the prototype
+ if (props.includes) {
+ checkDeprecatedMixinEvents(props.includes);
+ extend.apply(null, [proto].concat(props.includes));
+ delete props.includes;
+ }
+
+ // merge options
+ if (proto.options) {
+ props.options = extend(create(proto.options), props.options);
+ }
+
+ // mix given properties into the prototype
+ extend(proto, props);
+
+ proto._initHooks = [];
+
+ // add method for calling all hooks
+ proto.callInitHooks = function () {
+
+ if (this._initHooksCalled) { return; }
+
+ if (parentProto.callInitHooks) {
+ parentProto.callInitHooks.call(this);
+ }
+
+ this._initHooksCalled = true;
+
+ for (var i = 0, len = proto._initHooks.length; i < len; i++) {
+ proto._initHooks[i].call(this);
+ }
+ };
+
+ return NewClass;
+};
+
+
+// @function include(properties: Object): this
+// [Includes a mixin](#class-includes) into the current class.
+Class.include = function (props) {
+ extend(this.prototype, props);
+ return this;
+};
+
+// @function mergeOptions(options: Object): this
+// [Merges `options`](#class-options) into the defaults of the class.
+Class.mergeOptions = function (options) {
+ extend(this.prototype.options, options);
+ return this;
+};
+
+// @function addInitHook(fn: Function): this
+// Adds a [constructor hook](#class-constructor-hooks) to the class.
+Class.addInitHook = function (fn) { // (Function) || (String, args...)
+ var args = Array.prototype.slice.call(arguments, 1);
+
+ var init = typeof fn === 'function' ? fn : function () {
+ this[fn].apply(this, args);
+ };
+
+ this.prototype._initHooks = this.prototype._initHooks || [];
+ this.prototype._initHooks.push(init);
+ return this;
+};
+
+function checkDeprecatedMixinEvents(includes) {
+ if (typeof L === 'undefined' || !L || !L.Mixin) { return; }
+
+ includes = isArray(includes) ? includes : [includes];
+
+ for (var i = 0; i < includes.length; i++) {
+ if (includes[i] === L.Mixin.Events) {
+ console.warn('Deprecated include of L.Mixin.Events: ' +
+ 'this property will be removed in future releases, ' +
+ 'please inherit from L.Evented instead.', new Error().stack);
+ }
+ }
+}
+
+/*
+ * @class Evented
+ * @aka L.Evented
+ * @inherits Class
+ *
+ * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event).
+ *
+ * @example
+ *
+ * ```js
+ * map.on('click', function(e) {
+ * alert(e.latlng);
+ * } );
+ * ```
+ *
+ * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function:
+ *
+ * ```js
+ * function onClick(e) { ... }
+ *
+ * map.on('click', onClick);
+ * map.off('click', onClick);
+ * ```
+ */
+
+var Events = {
+ /* @method on(type: String, fn: Function, context?: Object): this
+ * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`).
+ *
+ * @alternative
+ * @method on(eventMap: Object): this
+ * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
+ */
+ on: function (types, fn, context) {
+
+ // types can be a map of types/handlers
+ if (typeof types === 'object') {
+ for (var type in types) {
+ // we don't process space-separated events here for performance;
+ // it's a hot path since Layer uses the on(obj) syntax
+ this._on(type, types[type], fn);
+ }
+
+ } else {
+ // types can be a string of space-separated words
+ types = splitWords(types);
+
+ for (var i = 0, len = types.length; i < len; i++) {
+ this._on(types[i], fn, context);
+ }
+ }
+
+ return this;
+ },
+
+ /* @method off(type: String, fn?: Function, context?: Object): this
+ * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener.
+ *
+ * @alternative
+ * @method off(eventMap: Object): this
+ * Removes a set of type/listener pairs.
+ *
+ * @alternative
+ * @method off: this
+ * Removes all listeners to all events on the object. This includes implicitly attached events.
+ */
+ off: function (types, fn, context) {
+
+ if (!types) {
+ // clear all listeners if called without arguments
+ delete this._events;
+
+ } else if (typeof types === 'object') {
+ for (var type in types) {
+ this._off(type, types[type], fn);
+ }
+
+ } else {
+ types = splitWords(types);
+
+ for (var i = 0, len = types.length; i < len; i++) {
+ this._off(types[i], fn, context);
+ }
+ }
+
+ return this;
+ },
+
+ // attach listener (without syntactic sugar now)
+ _on: function (type, fn, context) {
+ this._events = this._events || {};
+
+ /* get/init listeners for type */
+ var typeListeners = this._events[type];
+ if (!typeListeners) {
+ typeListeners = [];
+ this._events[type] = typeListeners;
+ }
+
+ if (context === this) {
+ // Less memory footprint.
+ context = undefined;
+ }
+ var newListener = {fn: fn, ctx: context},
+ listeners = typeListeners;
+
+ // check if fn already there
+ for (var i = 0, len = listeners.length; i < len; i++) {
+ if (listeners[i].fn === fn && listeners[i].ctx === context) {
+ return;
+ }
+ }
+
+ listeners.push(newListener);
+ },
+
+ _off: function (type, fn, context) {
+ var listeners,
+ i,
+ len;
+
+ if (!this._events) { return; }
+
+ listeners = this._events[type];
+
+ if (!listeners) {
+ return;
+ }
+
+ if (!fn) {
+ // Set all removed listeners to noop so they are not called if remove happens in fire
+ for (i = 0, len = listeners.length; i < len; i++) {
+ listeners[i].fn = falseFn;
+ }
+ // clear all listeners for a type if function isn't specified
+ delete this._events[type];
+ return;
+ }
+
+ if (context === this) {
+ context = undefined;
+ }
+
+ if (listeners) {
+
+ // find fn and remove it
+ for (i = 0, len = listeners.length; i < len; i++) {
+ var l = listeners[i];
+ if (l.ctx !== context) { continue; }
+ if (l.fn === fn) {
+
+ // set the removed listener to noop so that's not called if remove happens in fire
+ l.fn = falseFn;
+
+ if (this._firingCount) {
+ /* copy array in case events are being fired */
+ this._events[type] = listeners = listeners.slice();
+ }
+ listeners.splice(i, 1);
+
+ return;
+ }
+ }
+ }
+ },
+
+ // @method fire(type: String, data?: Object, propagate?: Boolean): this
+ // Fires an event of the specified type. You can optionally provide an data
+ // object — the first argument of the listener function will contain its
+ // properties. The event can optionally be propagated to event parents.
+ fire: function (type, data, propagate) {
+ if (!this.listens(type, propagate)) { return this; }
+
+ var event = extend({}, data, {
+ type: type,
+ target: this,
+ sourceTarget: data && data.sourceTarget || this
+ });
+
+ if (this._events) {
+ var listeners = this._events[type];
+
+ if (listeners) {
+ this._firingCount = (this._firingCount + 1) || 1;
+ for (var i = 0, len = listeners.length; i < len; i++) {
+ var l = listeners[i];
+ l.fn.call(l.ctx || this, event);
+ }
+
+ this._firingCount--;
+ }
+ }
+
+ if (propagate) {
+ // propagate the event to parents (set with addEventParent)
+ this._propagateEvent(event);
+ }
+
+ return this;
+ },
+
+ // @method listens(type: String): Boolean
+ // Returns `true` if a particular event type has any listeners attached to it.
+ listens: function (type, propagate) {
+ var listeners = this._events && this._events[type];
+ if (listeners && listeners.length) { return true; }
+
+ if (propagate) {
+ // also check parents for listeners if event propagates
+ for (var id in this._eventParents) {
+ if (this._eventParents[id].listens(type, propagate)) { return true; }
+ }
+ }
+ return false;
+ },
+
+ // @method once(…): this
+ // Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed.
+ once: function (types, fn, context) {
+
+ if (typeof types === 'object') {
+ for (var type in types) {
+ this.once(type, types[type], fn);
+ }
+ return this;
+ }
+
+ var handler = bind(function () {
+ this
+ .off(types, fn, context)
+ .off(types, handler, context);
+ }, this);
+
+ // add a listener that's executed once and removed after that
+ return this
+ .on(types, fn, context)
+ .on(types, handler, context);
+ },
+
+ // @method addEventParent(obj: Evented): this
+ // Adds an event parent - an `Evented` that will receive propagated events
+ addEventParent: function (obj) {
+ this._eventParents = this._eventParents || {};
+ this._eventParents[stamp(obj)] = obj;
+ return this;
+ },
+
+ // @method removeEventParent(obj: Evented): this
+ // Removes an event parent, so it will stop receiving propagated events
+ removeEventParent: function (obj) {
+ if (this._eventParents) {
+ delete this._eventParents[stamp(obj)];
+ }
+ return this;
+ },
+
+ _propagateEvent: function (e) {
+ for (var id in this._eventParents) {
+ this._eventParents[id].fire(e.type, extend({
+ layer: e.target,
+ propagatedFrom: e.target
+ }, e), true);
+ }
+ }
+};
+
+// aliases; we should ditch those eventually
+
+// @method addEventListener(…): this
+// Alias to [`on(…)`](#evented-on)
+Events.addEventListener = Events.on;
+
+// @method removeEventListener(…): this
+// Alias to [`off(…)`](#evented-off)
+
+// @method clearAllEventListeners(…): this
+// Alias to [`off()`](#evented-off)
+Events.removeEventListener = Events.clearAllEventListeners = Events.off;
+
+// @method addOneTimeEventListener(…): this
+// Alias to [`once(…)`](#evented-once)
+Events.addOneTimeEventListener = Events.once;
+
+// @method fireEvent(…): this
+// Alias to [`fire(…)`](#evented-fire)
+Events.fireEvent = Events.fire;
+
+// @method hasEventListeners(…): Boolean
+// Alias to [`listens(…)`](#evented-listens)
+Events.hasEventListeners = Events.listens;
+
+var Evented = Class.extend(Events);
+
+/*
+ * @class Point
+ * @aka L.Point
+ *
+ * Represents a point with `x` and `y` coordinates in pixels.
+ *
+ * @example
+ *
+ * ```js
+ * var point = L.point(200, 300);
+ * ```
+ *
+ * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent:
+ *
+ * ```js
+ * map.panBy([200, 300]);
+ * map.panBy(L.point(200, 300));
+ * ```
+ *
+ * Note that `Point` does not inherit from Leafet's `Class` object,
+ * which means new classes can't inherit from it, and new methods
+ * can't be added to it with the `include` function.
+ */
+
+function Point(x, y, round) {
+ // @property x: Number; The `x` coordinate of the point
+ this.x = (round ? Math.round(x) : x);
+ // @property y: Number; The `y` coordinate of the point
+ this.y = (round ? Math.round(y) : y);
+}
+
+var trunc = Math.trunc || function (v) {
+ return v > 0 ? Math.floor(v) : Math.ceil(v);
+};
+
+Point.prototype = {
+
+ // @method clone(): Point
+ // Returns a copy of the current point.
+ clone: function () {
+ return new Point(this.x, this.y);
+ },
+
+ // @method add(otherPoint: Point): Point
+ // Returns the result of addition of the current and the given points.
+ add: function (point) {
+ // non-destructive, returns a new point
+ return this.clone()._add(toPoint(point));
+ },
+
+ _add: function (point) {
+ // destructive, used directly for performance in situations where it's safe to modify existing point
+ this.x += point.x;
+ this.y += point.y;
+ return this;
+ },
+
+ // @method subtract(otherPoint: Point): Point
+ // Returns the result of subtraction of the given point from the current.
+ subtract: function (point) {
+ return this.clone()._subtract(toPoint(point));
+ },
+
+ _subtract: function (point) {
+ this.x -= point.x;
+ this.y -= point.y;
+ return this;
+ },
+
+ // @method divideBy(num: Number): Point
+ // Returns the result of division of the current point by the given number.
+ divideBy: function (num) {
+ return this.clone()._divideBy(num);
+ },
+
+ _divideBy: function (num) {
+ this.x /= num;
+ this.y /= num;
+ return this;
+ },
+
+ // @method multiplyBy(num: Number): Point
+ // Returns the result of multiplication of the current point by the given number.
+ multiplyBy: function (num) {
+ return this.clone()._multiplyBy(num);
+ },
+
+ _multiplyBy: function (num) {
+ this.x *= num;
+ this.y *= num;
+ return this;
+ },
+
+ // @method scaleBy(scale: Point): Point
+ // Multiply each coordinate of the current point by each coordinate of
+ // `scale`. In linear algebra terms, multiply the point by the
+ // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation)
+ // defined by `scale`.
+ scaleBy: function (point) {
+ return new Point(this.x * point.x, this.y * point.y);
+ },
+
+ // @method unscaleBy(scale: Point): Point
+ // Inverse of `scaleBy`. Divide each coordinate of the current point by
+ // each coordinate of `scale`.
+ unscaleBy: function (point) {
+ return new Point(this.x / point.x, this.y / point.y);
+ },
+
+ // @method round(): Point
+ // Returns a copy of the current point with rounded coordinates.
+ round: function () {
+ return this.clone()._round();
+ },
+
+ _round: function () {
+ this.x = Math.round(this.x);
+ this.y = Math.round(this.y);
+ return this;
+ },
+
+ // @method floor(): Point
+ // Returns a copy of the current point with floored coordinates (rounded down).
+ floor: function () {
+ return this.clone()._floor();
+ },
+
+ _floor: function () {
+ this.x = Math.floor(this.x);
+ this.y = Math.floor(this.y);
+ return this;
+ },
+
+ // @method ceil(): Point
+ // Returns a copy of the current point with ceiled coordinates (rounded up).
+ ceil: function () {
+ return this.clone()._ceil();
+ },
+
+ _ceil: function () {
+ this.x = Math.ceil(this.x);
+ this.y = Math.ceil(this.y);
+ return this;
+ },
+
+ // @method trunc(): Point
+ // Returns a copy of the current point with truncated coordinates (rounded towards zero).
+ trunc: function () {
+ return this.clone()._trunc();
+ },
+
+ _trunc: function () {
+ this.x = trunc(this.x);
+ this.y = trunc(this.y);
+ return this;
+ },
+
+ // @method distanceTo(otherPoint: Point): Number
+ // Returns the cartesian distance between the current and the given points.
+ distanceTo: function (point) {
+ point = toPoint(point);
+
+ var x = point.x - this.x,
+ y = point.y - this.y;
+
+ return Math.sqrt(x * x + y * y);
+ },
+
+ // @method equals(otherPoint: Point): Boolean
+ // Returns `true` if the given point has the same coordinates.
+ equals: function (point) {
+ point = toPoint(point);
+
+ return point.x === this.x &&
+ point.y === this.y;
+ },
+
+ // @method contains(otherPoint: Point): Boolean
+ // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values).
+ contains: function (point) {
+ point = toPoint(point);
+
+ return Math.abs(point.x) <= Math.abs(this.x) &&
+ Math.abs(point.y) <= Math.abs(this.y);
+ },
+
+ // @method toString(): String
+ // Returns a string representation of the point for debugging purposes.
+ toString: function () {
+ return 'Point(' +
+ formatNum(this.x) + ', ' +
+ formatNum(this.y) + ')';
+ }
+};
+
+// @factory L.point(x: Number, y: Number, round?: Boolean)
+// Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values.
+
+// @alternative
+// @factory L.point(coords: Number[])
+// Expects an array of the form `[x, y]` instead.
+
+// @alternative
+// @factory L.point(coords: Object)
+// Expects a plain object of the form `{x: Number, y: Number}` instead.
+function toPoint(x, y, round) {
+ if (x instanceof Point) {
+ return x;
+ }
+ if (isArray(x)) {
+ return new Point(x[0], x[1]);
+ }
+ if (x === undefined || x === null) {
+ return x;
+ }
+ if (typeof x === 'object' && 'x' in x && 'y' in x) {
+ return new Point(x.x, x.y);
+ }
+ return new Point(x, y, round);
+}
+
+/*
+ * @class Bounds
+ * @aka L.Bounds
+ *
+ * Represents a rectangular area in pixel coordinates.
+ *
+ * @example
+ *
+ * ```js
+ * var p1 = L.point(10, 10),
+ * p2 = L.point(40, 60),
+ * bounds = L.bounds(p1, p2);
+ * ```
+ *
+ * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:
+ *
+ * ```js
+ * otherBounds.intersects([[10, 10], [40, 60]]);
+ * ```
+ *
+ * Note that `Bounds` does not inherit from Leafet's `Class` object,
+ * which means new classes can't inherit from it, and new methods
+ * can't be added to it with the `include` function.
+ */
+
+function Bounds(a, b) {
+ if (!a) { return; }
+
+ var points = b ? [a, b] : a;
+
+ for (var i = 0, len = points.length; i < len; i++) {
+ this.extend(points[i]);
+ }
+}
+
+Bounds.prototype = {
+ // @method extend(point: Point): this
+ // Extends the bounds to contain the given point.
+ extend: function (point) { // (Point)
+ point = toPoint(point);
+
+ // @property min: Point
+ // The top left corner of the rectangle.
+ // @property max: Point
+ // The bottom right corner of the rectangle.
+ if (!this.min && !this.max) {
+ this.min = point.clone();
+ this.max = point.clone();
+ } else {
+ this.min.x = Math.min(point.x, this.min.x);
+ this.max.x = Math.max(point.x, this.max.x);
+ this.min.y = Math.min(point.y, this.min.y);
+ this.max.y = Math.max(point.y, this.max.y);
+ }
+ return this;
+ },
+
+ // @method getCenter(round?: Boolean): Point
+ // Returns the center point of the bounds.
+ getCenter: function (round) {
+ return new Point(
+ (this.min.x + this.max.x) / 2,
+ (this.min.y + this.max.y) / 2, round);
+ },
+
+ // @method getBottomLeft(): Point
+ // Returns the bottom-left point of the bounds.
+ getBottomLeft: function () {
+ return new Point(this.min.x, this.max.y);
+ },
+
+ // @method getTopRight(): Point
+ // Returns the top-right point of the bounds.
+ getTopRight: function () { // -> Point
+ return new Point(this.max.x, this.min.y);
+ },
+
+ // @method getTopLeft(): Point
+ // Returns the top-left point of the bounds (i.e. [`this.min`](#bounds-min)).
+ getTopLeft: function () {
+ return this.min; // left, top
+ },
+
+ // @method getBottomRight(): Point
+ // Returns the bottom-right point of the bounds (i.e. [`this.max`](#bounds-max)).
+ getBottomRight: function () {
+ return this.max; // right, bottom
+ },
+
+ // @method getSize(): Point
+ // Returns the size of the given bounds
+ getSize: function () {
+ return this.max.subtract(this.min);
+ },
+
+ // @method contains(otherBounds: Bounds): Boolean
+ // Returns `true` if the rectangle contains the given one.
+ // @alternative
+ // @method contains(point: Point): Boolean
+ // Returns `true` if the rectangle contains the given point.
+ contains: function (obj) {
+ var min, max;
+
+ if (typeof obj[0] === 'number' || obj instanceof Point) {
+ obj = toPoint(obj);
+ } else {
+ obj = toBounds(obj);
+ }
+
+ if (obj instanceof Bounds) {
+ min = obj.min;
+ max = obj.max;
+ } else {
+ min = max = obj;
+ }
+
+ return (min.x >= this.min.x) &&
+ (max.x <= this.max.x) &&
+ (min.y >= this.min.y) &&
+ (max.y <= this.max.y);
+ },
+
+ // @method intersects(otherBounds: Bounds): Boolean
+ // Returns `true` if the rectangle intersects the given bounds. Two bounds
+ // intersect if they have at least one point in common.
+ intersects: function (bounds) { // (Bounds) -> Boolean
+ bounds = toBounds(bounds);
+
+ var min = this.min,
+ max = this.max,
+ min2 = bounds.min,
+ max2 = bounds.max,
+ xIntersects = (max2.x >= min.x) && (min2.x <= max.x),
+ yIntersects = (max2.y >= min.y) && (min2.y <= max.y);
+
+ return xIntersects && yIntersects;
+ },
+
+ // @method overlaps(otherBounds: Bounds): Boolean
+ // Returns `true` if the rectangle overlaps the given bounds. Two bounds
+ // overlap if their intersection is an area.
+ overlaps: function (bounds) { // (Bounds) -> Boolean
+ bounds = toBounds(bounds);
+
+ var min = this.min,
+ max = this.max,
+ min2 = bounds.min,
+ max2 = bounds.max,
+ xOverlaps = (max2.x > min.x) && (min2.x < max.x),
+ yOverlaps = (max2.y > min.y) && (min2.y < max.y);
+
+ return xOverlaps && yOverlaps;
+ },
+
+ isValid: function () {
+ return !!(this.min && this.max);
+ }
+};
+
+
+// @factory L.bounds(corner1: Point, corner2: Point)
+// Creates a Bounds object from two corners coordinate pairs.
+// @alternative
+// @factory L.bounds(points: Point[])
+// Creates a Bounds object from the given array of points.
+function toBounds(a, b) {
+ if (!a || a instanceof Bounds) {
+ return a;
+ }
+ return new Bounds(a, b);
+}
+
+/*
+ * @class LatLngBounds
+ * @aka L.LatLngBounds
+ *
+ * Represents a rectangular geographical area on a map.
+ *
+ * @example
+ *
+ * ```js
+ * var corner1 = L.latLng(40.712, -74.227),
+ * corner2 = L.latLng(40.774, -74.125),
+ * bounds = L.latLngBounds(corner1, corner2);
+ * ```
+ *
+ * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:
+ *
+ * ```js
+ * map.fitBounds([
+ * [40.712, -74.227],
+ * [40.774, -74.125]
+ * ]);
+ * ```
+ *
+ * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range.
+ *
+ * Note that `LatLngBounds` does not inherit from Leafet's `Class` object,
+ * which means new classes can't inherit from it, and new methods
+ * can't be added to it with the `include` function.
+ */
+
+function LatLngBounds(corner1, corner2) { // (LatLng, LatLng) or (LatLng[])
+ if (!corner1) { return; }
+
+ var latlngs = corner2 ? [corner1, corner2] : corner1;
+
+ for (var i = 0, len = latlngs.length; i < len; i++) {
+ this.extend(latlngs[i]);
+ }
+}
+
+LatLngBounds.prototype = {
+
+ // @method extend(latlng: LatLng): this
+ // Extend the bounds to contain the given point
+
+ // @alternative
+ // @method extend(otherBounds: LatLngBounds): this
+ // Extend the bounds to contain the given bounds
+ extend: function (obj) {
+ var sw = this._southWest,
+ ne = this._northEast,
+ sw2, ne2;
+
+ if (obj instanceof LatLng) {
+ sw2 = obj;
+ ne2 = obj;
+
+ } else if (obj instanceof LatLngBounds) {
+ sw2 = obj._southWest;
+ ne2 = obj._northEast;
+
+ if (!sw2 || !ne2) { return this; }
+
+ } else {
+ return obj ? this.extend(toLatLng(obj) || toLatLngBounds(obj)) : this;
+ }
+
+ if (!sw && !ne) {
+ this._southWest = new LatLng(sw2.lat, sw2.lng);
+ this._northEast = new LatLng(ne2.lat, ne2.lng);
+ } else {
+ sw.lat = Math.min(sw2.lat, sw.lat);
+ sw.lng = Math.min(sw2.lng, sw.lng);
+ ne.lat = Math.max(ne2.lat, ne.lat);
+ ne.lng = Math.max(ne2.lng, ne.lng);
+ }
+
+ return this;
+ },
+
+ // @method pad(bufferRatio: Number): LatLngBounds
+ // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction.
+ // For example, a ratio of 0.5 extends the bounds by 50% in each direction.
+ // Negative values will retract the bounds.
+ pad: function (bufferRatio) {
+ var sw = this._southWest,
+ ne = this._northEast,
+ heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,
+ widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;
+
+ return new LatLngBounds(
+ new LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),
+ new LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));
+ },
+
+ // @method getCenter(): LatLng
+ // Returns the center point of the bounds.
+ getCenter: function () {
+ return new LatLng(
+ (this._southWest.lat + this._northEast.lat) / 2,
+ (this._southWest.lng + this._northEast.lng) / 2);
+ },
+
+ // @method getSouthWest(): LatLng
+ // Returns the south-west point of the bounds.
+ getSouthWest: function () {
+ return this._southWest;
+ },
+
+ // @method getNorthEast(): LatLng
+ // Returns the north-east point of the bounds.
+ getNorthEast: function () {
+ return this._northEast;
+ },
+
+ // @method getNorthWest(): LatLng
+ // Returns the north-west point of the bounds.
+ getNorthWest: function () {
+ return new LatLng(this.getNorth(), this.getWest());
+ },
+
+ // @method getSouthEast(): LatLng
+ // Returns the south-east point of the bounds.
+ getSouthEast: function () {
+ return new LatLng(this.getSouth(), this.getEast());
+ },
+
+ // @method getWest(): Number
+ // Returns the west longitude of the bounds
+ getWest: function () {
+ return this._southWest.lng;
+ },
+
+ // @method getSouth(): Number
+ // Returns the south latitude of the bounds
+ getSouth: function () {
+ return this._southWest.lat;
+ },
+
+ // @method getEast(): Number
+ // Returns the east longitude of the bounds
+ getEast: function () {
+ return this._northEast.lng;
+ },
+
+ // @method getNorth(): Number
+ // Returns the north latitude of the bounds
+ getNorth: function () {
+ return this._northEast.lat;
+ },
+
+ // @method contains(otherBounds: LatLngBounds): Boolean
+ // Returns `true` if the rectangle contains the given one.
+
+ // @alternative
+ // @method contains (latlng: LatLng): Boolean
+ // Returns `true` if the rectangle contains the given point.
+ contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean
+ if (typeof obj[0] === 'number' || obj instanceof LatLng || 'lat' in obj) {
+ obj = toLatLng(obj);
+ } else {
+ obj = toLatLngBounds(obj);
+ }
+
+ var sw = this._southWest,
+ ne = this._northEast,
+ sw2, ne2;
+
+ if (obj instanceof LatLngBounds) {
+ sw2 = obj.getSouthWest();
+ ne2 = obj.getNorthEast();
+ } else {
+ sw2 = ne2 = obj;
+ }
+
+ return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) &&
+ (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng);
+ },
+
+ // @method intersects(otherBounds: LatLngBounds): Boolean
+ // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common.
+ intersects: function (bounds) {
+ bounds = toLatLngBounds(bounds);
+
+ var sw = this._southWest,
+ ne = this._northEast,
+ sw2 = bounds.getSouthWest(),
+ ne2 = bounds.getNorthEast(),
+
+ latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat),
+ lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng);
+
+ return latIntersects && lngIntersects;
+ },
+
+ // @method overlaps(otherBounds: Bounds): Boolean
+ // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area.
+ overlaps: function (bounds) {
+ bounds = toLatLngBounds(bounds);
+
+ var sw = this._southWest,
+ ne = this._northEast,
+ sw2 = bounds.getSouthWest(),
+ ne2 = bounds.getNorthEast(),
+
+ latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat),
+ lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng);
+
+ return latOverlaps && lngOverlaps;
+ },
+
+ // @method toBBoxString(): String
+ // Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data.
+ toBBoxString: function () {
+ return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(',');
+ },
+
+ // @method equals(otherBounds: LatLngBounds, maxMargin?: Number): Boolean
+ // Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds. The margin of error can be overridden by setting `maxMargin` to a small number.
+ equals: function (bounds, maxMargin) {
+ if (!bounds) { return false; }
+
+ bounds = toLatLngBounds(bounds);
+
+ return this._southWest.equals(bounds.getSouthWest(), maxMargin) &&
+ this._northEast.equals(bounds.getNorthEast(), maxMargin);
+ },
+
+ // @method isValid(): Boolean
+ // Returns `true` if the bounds are properly initialized.
+ isValid: function () {
+ return !!(this._southWest && this._northEast);
+ }
+};
+
+// TODO International date line?
+
+// @factory L.latLngBounds(corner1: LatLng, corner2: LatLng)
+// Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle.
+
+// @alternative
+// @factory L.latLngBounds(latlngs: LatLng[])
+// Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds).
+function toLatLngBounds(a, b) {
+ if (a instanceof LatLngBounds) {
+ return a;
+ }
+ return new LatLngBounds(a, b);
+}
+
+/* @class LatLng
+ * @aka L.LatLng
+ *
+ * Represents a geographical point with a certain latitude and longitude.
+ *
+ * @example
+ *
+ * ```
+ * var latlng = L.latLng(50.5, 30.5);
+ * ```
+ *
+ * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent:
+ *
+ * ```
+ * map.panTo([50, 30]);
+ * map.panTo({lon: 30, lat: 50});
+ * map.panTo({lat: 50, lng: 30});
+ * map.panTo(L.latLng(50, 30));
+ * ```
+ *
+ * Note that `LatLng` does not inherit from Leaflet's `Class` object,
+ * which means new classes can't inherit from it, and new methods
+ * can't be added to it with the `include` function.
+ */
+
+function LatLng(lat, lng, alt) {
+ if (isNaN(lat) || isNaN(lng)) {
+ throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')');
+ }
+
+ // @property lat: Number
+ // Latitude in degrees
+ this.lat = +lat;
+
+ // @property lng: Number
+ // Longitude in degrees
+ this.lng = +lng;
+
+ // @property alt: Number
+ // Altitude in meters (optional)
+ if (alt !== undefined) {
+ this.alt = +alt;
+ }
+}
+
+LatLng.prototype = {
+ // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean
+ // Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overridden by setting `maxMargin` to a small number.
+ equals: function (obj, maxMargin) {
+ if (!obj) { return false; }
+
+ obj = toLatLng(obj);
+
+ var margin = Math.max(
+ Math.abs(this.lat - obj.lat),
+ Math.abs(this.lng - obj.lng));
+
+ return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin);
+ },
+
+ // @method toString(): String
+ // Returns a string representation of the point (for debugging purposes).
+ toString: function (precision) {
+ return 'LatLng(' +
+ formatNum(this.lat, precision) + ', ' +
+ formatNum(this.lng, precision) + ')';
+ },
+
+ // @method distanceTo(otherLatLng: LatLng): Number
+ // Returns the distance (in meters) to the given `LatLng` calculated using the [Spherical Law of Cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines).
+ distanceTo: function (other) {
+ return Earth.distance(this, toLatLng(other));
+ },
+
+ // @method wrap(): LatLng
+ // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees.
+ wrap: function () {
+ return Earth.wrapLatLng(this);
+ },
+
+ // @method toBounds(sizeInMeters: Number): LatLngBounds
+ // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`.
+ toBounds: function (sizeInMeters) {
+ var latAccuracy = 180 * sizeInMeters / 40075017,
+ lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat);
+
+ return toLatLngBounds(
+ [this.lat - latAccuracy, this.lng - lngAccuracy],
+ [this.lat + latAccuracy, this.lng + lngAccuracy]);
+ },
+
+ clone: function () {
+ return new LatLng(this.lat, this.lng, this.alt);
+ }
+};
+
+
+
+// @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng
+// Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude).
+
+// @alternative
+// @factory L.latLng(coords: Array): LatLng
+// Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead.
+
+// @alternative
+// @factory L.latLng(coords: Object): LatLng
+// Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead.
+
+function toLatLng(a, b, c) {
+ if (a instanceof LatLng) {
+ return a;
+ }
+ if (isArray(a) && typeof a[0] !== 'object') {
+ if (a.length === 3) {
+ return new LatLng(a[0], a[1], a[2]);
+ }
+ if (a.length === 2) {
+ return new LatLng(a[0], a[1]);
+ }
+ return null;
+ }
+ if (a === undefined || a === null) {
+ return a;
+ }
+ if (typeof a === 'object' && 'lat' in a) {
+ return new LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt);
+ }
+ if (b === undefined) {
+ return null;
+ }
+ return new LatLng(a, b, c);
+}
+
+/*
+ * @namespace CRS
+ * @crs L.CRS.Base
+ * Object that defines coordinate reference systems for projecting
+ * geographical points into pixel (screen) coordinates and back (and to
+ * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See
+ * [spatial reference system](http://en.wikipedia.org/wiki/Coordinate_reference_system).
+ *
+ * Leaflet defines the most usual CRSs by default. If you want to use a
+ * CRS not defined by default, take a look at the
+ * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin.
+ *
+ * Note that the CRS instances do not inherit from Leafet's `Class` object,
+ * and can't be instantiated. Also, new classes can't inherit from them,
+ * and methods can't be added to them with the `include` function.
+ */
+
+var CRS = {
+ // @method latLngToPoint(latlng: LatLng, zoom: Number): Point
+ // Projects geographical coordinates into pixel coordinates for a given zoom.
+ latLngToPoint: function (latlng, zoom) {
+ var projectedPoint = this.projection.project(latlng),
+ scale = this.scale(zoom);
+
+ return this.transformation._transform(projectedPoint, scale);
+ },
+
+ // @method pointToLatLng(point: Point, zoom: Number): LatLng
+ // The inverse of `latLngToPoint`. Projects pixel coordinates on a given
+ // zoom into geographical coordinates.
+ pointToLatLng: function (point, zoom) {
+ var scale = this.scale(zoom),
+ untransformedPoint = this.transformation.untransform(point, scale);
+
+ return this.projection.unproject(untransformedPoint);
+ },
+
+ // @method project(latlng: LatLng): Point
+ // Projects geographical coordinates into coordinates in units accepted for
+ // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services).
+ project: function (latlng) {
+ return this.projection.project(latlng);
+ },
+
+ // @method unproject(point: Point): LatLng
+ // Given a projected coordinate returns the corresponding LatLng.
+ // The inverse of `project`.
+ unproject: function (point) {
+ return this.projection.unproject(point);
+ },
+
+ // @method scale(zoom: Number): Number
+ // Returns the scale used when transforming projected coordinates into
+ // pixel coordinates for a particular zoom. For example, it returns
+ // `256 * 2^zoom` for Mercator-based CRS.
+ scale: function (zoom) {
+ return 256 * Math.pow(2, zoom);
+ },
+
+ // @method zoom(scale: Number): Number
+ // Inverse of `scale()`, returns the zoom level corresponding to a scale
+ // factor of `scale`.
+ zoom: function (scale) {
+ return Math.log(scale / 256) / Math.LN2;
+ },
+
+ // @method getProjectedBounds(zoom: Number): Bounds
+ // Returns the projection's bounds scaled and transformed for the provided `zoom`.
+ getProjectedBounds: function (zoom) {
+ if (this.infinite) { return null; }
+
+ var b = this.projection.bounds,
+ s = this.scale(zoom),
+ min = this.transformation.transform(b.min, s),
+ max = this.transformation.transform(b.max, s);
+
+ return new Bounds(min, max);
+ },
+
+ // @method distance(latlng1: LatLng, latlng2: LatLng): Number
+ // Returns the distance between two geographical coordinates.
+
+ // @property code: String
+ // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`)
+ //
+ // @property wrapLng: Number[]
+ // An array of two numbers defining whether the longitude (horizontal) coordinate
+ // axis wraps around a given range and how. Defaults to `[-180, 180]` in most
+ // geographical CRSs. If `undefined`, the longitude axis does not wrap around.
+ //
+ // @property wrapLat: Number[]
+ // Like `wrapLng`, but for the latitude (vertical) axis.
+
+ // wrapLng: [min, max],
+ // wrapLat: [min, max],
+
+ // @property infinite: Boolean
+ // If true, the coordinate space will be unbounded (infinite in both axes)
+ infinite: false,
+
+ // @method wrapLatLng(latlng: LatLng): LatLng
+ // Returns a `LatLng` where lat and lng has been wrapped according to the
+ // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds.
+ wrapLatLng: function (latlng) {
+ var lng = this.wrapLng ? wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng,
+ lat = this.wrapLat ? wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat,
+ alt = latlng.alt;
+
+ return new LatLng(lat, lng, alt);
+ },
+
+ // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds
+ // Returns a `LatLngBounds` with the same size as the given one, ensuring
+ // that its center is within the CRS's bounds.
+ // Only accepts actual `L.LatLngBounds` instances, not arrays.
+ wrapLatLngBounds: function (bounds) {
+ var center = bounds.getCenter(),
+ newCenter = this.wrapLatLng(center),
+ latShift = center.lat - newCenter.lat,
+ lngShift = center.lng - newCenter.lng;
+
+ if (latShift === 0 && lngShift === 0) {
+ return bounds;
+ }
+
+ var sw = bounds.getSouthWest(),
+ ne = bounds.getNorthEast(),
+ newSw = new LatLng(sw.lat - latShift, sw.lng - lngShift),
+ newNe = new LatLng(ne.lat - latShift, ne.lng - lngShift);
+
+ return new LatLngBounds(newSw, newNe);
+ }
+};
+
+/*
+ * @namespace CRS
+ * @crs L.CRS.Earth
+ *
+ * Serves as the base for CRS that are global such that they cover the earth.
+ * Can only be used as the base for other CRS and cannot be used directly,
+ * since it does not have a `code`, `projection` or `transformation`. `distance()` returns
+ * meters.
+ */
+
+var Earth = extend({}, CRS, {
+ wrapLng: [-180, 180],
+
+ // Mean Earth Radius, as recommended for use by
+ // the International Union of Geodesy and Geophysics,
+ // see http://rosettacode.org/wiki/Haversine_formula
+ R: 6371000,
+
+ // distance between two geographical points using spherical law of cosines approximation
+ distance: function (latlng1, latlng2) {
+ var rad = Math.PI / 180,
+ lat1 = latlng1.lat * rad,
+ lat2 = latlng2.lat * rad,
+ sinDLat = Math.sin((latlng2.lat - latlng1.lat) * rad / 2),
+ sinDLon = Math.sin((latlng2.lng - latlng1.lng) * rad / 2),
+ a = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon,
+ c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ return this.R * c;
+ }
+});
+
+/*
+ * @namespace Projection
+ * @projection L.Projection.SphericalMercator
+ *
+ * Spherical Mercator projection — the most common projection for online maps,
+ * used by almost all free and commercial tile providers. Assumes that Earth is
+ * a sphere. Used by the `EPSG:3857` CRS.
+ */
+
+var earthRadius = 6378137;
+
+var SphericalMercator = {
+
+ R: earthRadius,
+ MAX_LATITUDE: 85.0511287798,
+
+ project: function (latlng) {
+ var d = Math.PI / 180,
+ max = this.MAX_LATITUDE,
+ lat = Math.max(Math.min(max, latlng.lat), -max),
+ sin = Math.sin(lat * d);
+
+ return new Point(
+ this.R * latlng.lng * d,
+ this.R * Math.log((1 + sin) / (1 - sin)) / 2);
+ },
+
+ unproject: function (point) {
+ var d = 180 / Math.PI;
+
+ return new LatLng(
+ (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d,
+ point.x * d / this.R);
+ },
+
+ bounds: (function () {
+ var d = earthRadius * Math.PI;
+ return new Bounds([-d, -d], [d, d]);
+ })()
+};
+
+/*
+ * @class Transformation
+ * @aka L.Transformation
+ *
+ * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d`
+ * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing
+ * the reverse. Used by Leaflet in its projections code.
+ *
+ * @example
+ *
+ * ```js
+ * var transformation = L.transformation(2, 5, -1, 10),
+ * p = L.point(1, 2),
+ * p2 = transformation.transform(p), // L.point(7, 8)
+ * p3 = transformation.untransform(p2); // L.point(1, 2)
+ * ```
+ */
+
+
+// factory new L.Transformation(a: Number, b: Number, c: Number, d: Number)
+// Creates a `Transformation` object with the given coefficients.
+function Transformation(a, b, c, d) {
+ if (isArray(a)) {
+ // use array properties
+ this._a = a[0];
+ this._b = a[1];
+ this._c = a[2];
+ this._d = a[3];
+ return;
+ }
+ this._a = a;
+ this._b = b;
+ this._c = c;
+ this._d = d;
+}
+
+Transformation.prototype = {
+ // @method transform(point: Point, scale?: Number): Point
+ // Returns a transformed point, optionally multiplied by the given scale.
+ // Only accepts actual `L.Point` instances, not arrays.
+ transform: function (point, scale) { // (Point, Number) -> Point
+ return this._transform(point.clone(), scale);
+ },
+
+ // destructive transform (faster)
+ _transform: function (point, scale) {
+ scale = scale || 1;
+ point.x = scale * (this._a * point.x + this._b);
+ point.y = scale * (this._c * point.y + this._d);
+ return point;
+ },
+
+ // @method untransform(point: Point, scale?: Number): Point
+ // Returns the reverse transformation of the given point, optionally divided
+ // by the given scale. Only accepts actual `L.Point` instances, not arrays.
+ untransform: function (point, scale) {
+ scale = scale || 1;
+ return new Point(
+ (point.x / scale - this._b) / this._a,
+ (point.y / scale - this._d) / this._c);
+ }
+};
+
+// factory L.transformation(a: Number, b: Number, c: Number, d: Number)
+
+// @factory L.transformation(a: Number, b: Number, c: Number, d: Number)
+// Instantiates a Transformation object with the given coefficients.
+
+// @alternative
+// @factory L.transformation(coefficients: Array): Transformation
+// Expects an coefficients array of the form
+// `[a: Number, b: Number, c: Number, d: Number]`.
+
+function toTransformation(a, b, c, d) {
+ return new Transformation(a, b, c, d);
+}
+
+/*
+ * @namespace CRS
+ * @crs L.CRS.EPSG3857
+ *
+ * The most common CRS for online maps, used by almost all free and commercial
+ * tile providers. Uses Spherical Mercator projection. Set in by default in
+ * Map's `crs` option.
+ */
+
+var EPSG3857 = extend({}, Earth, {
+ code: 'EPSG:3857',
+ projection: SphericalMercator,
+
+ transformation: (function () {
+ var scale = 0.5 / (Math.PI * SphericalMercator.R);
+ return toTransformation(scale, 0.5, -scale, 0.5);
+ }())
+});
+
+var EPSG900913 = extend({}, EPSG3857, {
+ code: 'EPSG:900913'
+});
+
+// @namespace SVG; @section
+// There are several static functions which can be called without instantiating L.SVG:
+
+// @function create(name: String): SVGElement
+// Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement),
+// corresponding to the class name passed. For example, using 'line' will return
+// an instance of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement).
+function svgCreate(name) {
+ return document.createElementNS('http://www.w3.org/2000/svg', name);
+}
+
+// @function pointsToPath(rings: Point[], closed: Boolean): String
+// Generates a SVG path string for multiple rings, with each ring turning
+// into "M..L..L.." instructions
+function pointsToPath(rings, closed) {
+ var str = '',
+ i, j, len, len2, points, p;
+
+ for (i = 0, len = rings.length; i < len; i++) {
+ points = rings[i];
+
+ for (j = 0, len2 = points.length; j < len2; j++) {
+ p = points[j];
+ str += (j ? 'L' : 'M') + p.x + ' ' + p.y;
+ }
+
+ // closes the ring for polygons; "x" is VML syntax
+ str += closed ? (svg ? 'z' : 'x') : '';
+ }
+
+ // SVG complains about empty path strings
+ return str || 'M0 0';
+}
+
+/*
+ * @namespace Browser
+ * @aka L.Browser
+ *
+ * A namespace with static properties for browser/feature detection used by Leaflet internally.
+ *
+ * @example
+ *
+ * ```js
+ * if (L.Browser.ielt9) {
+ * alert('Upgrade your browser, dude!');
+ * }
+ * ```
+ */
+
+var style$1 = document.documentElement.style;
+
+// @property ie: Boolean; `true` for all Internet Explorer versions (not Edge).
+var ie = 'ActiveXObject' in window;
+
+// @property ielt9: Boolean; `true` for Internet Explorer versions less than 9.
+var ielt9 = ie && !document.addEventListener;
+
+// @property edge: Boolean; `true` for the Edge web browser.
+var edge = 'msLaunchUri' in navigator && !('documentMode' in document);
+
+// @property webkit: Boolean;
+// `true` for webkit-based browsers like Chrome and Safari (including mobile versions).
+var webkit = userAgentContains('webkit');
+
+// @property android: Boolean
+// `true` for any browser running on an Android platform.
+var android = userAgentContains('android');
+
+// @property android23: Boolean; `true` for browsers running on Android 2 or Android 3.
+var android23 = userAgentContains('android 2') || userAgentContains('android 3');
+
+/* See https://stackoverflow.com/a/17961266 for details on detecting stock Android */
+var webkitVer = parseInt(/WebKit\/([0-9]+)|$/.exec(navigator.userAgent)[1], 10); // also matches AppleWebKit
+// @property androidStock: Boolean; `true` for the Android stock browser (i.e. not Chrome)
+var androidStock = android && userAgentContains('Google') && webkitVer < 537 && !('AudioNode' in window);
+
+// @property opera: Boolean; `true` for the Opera browser
+var opera = !!window.opera;
+
+// @property chrome: Boolean; `true` for the Chrome browser.
+var chrome = userAgentContains('chrome');
+
+// @property gecko: Boolean; `true` for gecko-based browsers like Firefox.
+var gecko = userAgentContains('gecko') && !webkit && !opera && !ie;
+
+// @property safari: Boolean; `true` for the Safari browser.
+var safari = !chrome && userAgentContains('safari');
+
+var phantom = userAgentContains('phantom');
+
+// @property opera12: Boolean
+// `true` for the Opera browser supporting CSS transforms (version 12 or later).
+var opera12 = 'OTransition' in style$1;
+
+// @property win: Boolean; `true` when the browser is running in a Windows platform
+var win = navigator.platform.indexOf('Win') === 0;
+
+// @property ie3d: Boolean; `true` for all Internet Explorer versions supporting CSS transforms.
+var ie3d = ie && ('transition' in style$1);
+
+// @property webkit3d: Boolean; `true` for webkit-based browsers supporting CSS transforms.
+var webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23;
+
+// @property gecko3d: Boolean; `true` for gecko-based browsers supporting CSS transforms.
+var gecko3d = 'MozPerspective' in style$1;
+
+// @property any3d: Boolean
+// `true` for all browsers supporting CSS transforms.
+var any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantom;
+
+// @property mobile: Boolean; `true` for all browsers running in a mobile device.
+var mobile = typeof orientation !== 'undefined' || userAgentContains('mobile');
+
+// @property mobileWebkit: Boolean; `true` for all webkit-based browsers in a mobile device.
+var mobileWebkit = mobile && webkit;
+
+// @property mobileWebkit3d: Boolean
+// `true` for all webkit-based browsers in a mobile device supporting CSS transforms.
+var mobileWebkit3d = mobile && webkit3d;
+
+// @property msPointer: Boolean
+// `true` for browsers implementing the Microsoft touch events model (notably IE10).
+var msPointer = !window.PointerEvent && window.MSPointerEvent;
+
+// @property pointer: Boolean
+// `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx).
+var pointer = !webkit && !!(window.PointerEvent || msPointer);
+
+// @property touch: Boolean
+// `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events).
+// This does not necessarily mean that the browser is running in a computer with
+// a touchscreen, it only means that the browser is capable of understanding
+// touch events.
+var touch = !window.L_NO_TOUCH && (pointer || 'ontouchstart' in window ||
+ (window.DocumentTouch && document instanceof window.DocumentTouch));
+
+// @property mobileOpera: Boolean; `true` for the Opera browser in a mobile device.
+var mobileOpera = mobile && opera;
+
+// @property mobileGecko: Boolean
+// `true` for gecko-based browsers running in a mobile device.
+var mobileGecko = mobile && gecko;
+
+// @property retina: Boolean
+// `true` for browsers on a high-resolution "retina" screen or on any screen when browser's display zoom is more than 100%.
+var retina = (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1;
+
+// @property passiveEvents: Boolean
+// `true` for browsers that support passive events.
+var passiveEvents = (function () {
+ var supportsPassiveOption = false;
+ try {
+ var opts = Object.defineProperty({}, 'passive', {
+ get: function () {
+ supportsPassiveOption = true;
+ }
+ });
+ window.addEventListener('testPassiveEventSupport', falseFn, opts);
+ window.removeEventListener('testPassiveEventSupport', falseFn, opts);
+ } catch (e) {
+ // Errors can safely be ignored since this is only a browser support test.
+ }
+ return supportsPassiveOption;
+});
+
+// @property canvas: Boolean
+// `true` when the browser supports [``](https://developer.mozilla.org/docs/Web/API/Canvas_API).
+var canvas = (function () {
+ return !!document.createElement('canvas').getContext;
+}());
+
+// @property svg: Boolean
+// `true` when the browser supports [SVG](https://developer.mozilla.org/docs/Web/SVG).
+var svg = !!(document.createElementNS && svgCreate('svg').createSVGRect);
+
+// @property vml: Boolean
+// `true` if the browser supports [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language).
+var vml = !svg && (function () {
+ try {
+ var div = document.createElement('div');
+ div.innerHTML = ' ';
+
+ var shape = div.firstChild;
+ shape.style.behavior = 'url(#default#VML)';
+
+ return shape && (typeof shape.adj === 'object');
+
+ } catch (e) {
+ return false;
+ }
+}());
+
+
+function userAgentContains(str) {
+ return navigator.userAgent.toLowerCase().indexOf(str) >= 0;
+}
+
+
+var Browser = (Object.freeze || Object)({
+ ie: ie,
+ ielt9: ielt9,
+ edge: edge,
+ webkit: webkit,
+ android: android,
+ android23: android23,
+ androidStock: androidStock,
+ opera: opera,
+ chrome: chrome,
+ gecko: gecko,
+ safari: safari,
+ phantom: phantom,
+ opera12: opera12,
+ win: win,
+ ie3d: ie3d,
+ webkit3d: webkit3d,
+ gecko3d: gecko3d,
+ any3d: any3d,
+ mobile: mobile,
+ mobileWebkit: mobileWebkit,
+ mobileWebkit3d: mobileWebkit3d,
+ msPointer: msPointer,
+ pointer: pointer,
+ touch: touch,
+ mobileOpera: mobileOpera,
+ mobileGecko: mobileGecko,
+ retina: retina,
+ passiveEvents: passiveEvents,
+ canvas: canvas,
+ svg: svg,
+ vml: vml
+});
+
+/*
+ * Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices.
+ */
+
+
+var POINTER_DOWN = msPointer ? 'MSPointerDown' : 'pointerdown';
+var POINTER_MOVE = msPointer ? 'MSPointerMove' : 'pointermove';
+var POINTER_UP = msPointer ? 'MSPointerUp' : 'pointerup';
+var POINTER_CANCEL = msPointer ? 'MSPointerCancel' : 'pointercancel';
+var TAG_WHITE_LIST = ['INPUT', 'SELECT', 'OPTION'];
+
+var _pointers = {};
+var _pointerDocListener = false;
+
+// DomEvent.DoubleTap needs to know about this
+var _pointersCount = 0;
+
+// Provides a touch events wrapper for (ms)pointer events.
+// ref http://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890
+
+function addPointerListener(obj, type, handler, id) {
+ if (type === 'touchstart') {
+ _addPointerStart(obj, handler, id);
+
+ } else if (type === 'touchmove') {
+ _addPointerMove(obj, handler, id);
+
+ } else if (type === 'touchend') {
+ _addPointerEnd(obj, handler, id);
+ }
+
+ return this;
+}
+
+function removePointerListener(obj, type, id) {
+ var handler = obj['_leaflet_' + type + id];
+
+ if (type === 'touchstart') {
+ obj.removeEventListener(POINTER_DOWN, handler, false);
+
+ } else if (type === 'touchmove') {
+ obj.removeEventListener(POINTER_MOVE, handler, false);
+
+ } else if (type === 'touchend') {
+ obj.removeEventListener(POINTER_UP, handler, false);
+ obj.removeEventListener(POINTER_CANCEL, handler, false);
+ }
+
+ return this;
+}
+
+function _addPointerStart(obj, handler, id) {
+ var onDown = bind(function (e) {
+ if (e.pointerType !== 'mouse' && e.MSPOINTER_TYPE_MOUSE && e.pointerType !== e.MSPOINTER_TYPE_MOUSE) {
+ // In IE11, some touch events needs to fire for form controls, or
+ // the controls will stop working. We keep a whitelist of tag names that
+ // need these events. For other target tags, we prevent default on the event.
+ if (TAG_WHITE_LIST.indexOf(e.target.tagName) < 0) {
+ preventDefault(e);
+ } else {
+ return;
+ }
+ }
+
+ _handlePointer(e, handler);
+ });
+
+ obj['_leaflet_touchstart' + id] = onDown;
+ obj.addEventListener(POINTER_DOWN, onDown, false);
+
+ // need to keep track of what pointers and how many are active to provide e.touches emulation
+ if (!_pointerDocListener) {
+ // we listen documentElement as any drags that end by moving the touch off the screen get fired there
+ document.documentElement.addEventListener(POINTER_DOWN, _globalPointerDown, true);
+ document.documentElement.addEventListener(POINTER_MOVE, _globalPointerMove, true);
+ document.documentElement.addEventListener(POINTER_UP, _globalPointerUp, true);
+ document.documentElement.addEventListener(POINTER_CANCEL, _globalPointerUp, true);
+
+ _pointerDocListener = true;
+ }
+}
+
+function _globalPointerDown(e) {
+ _pointers[e.pointerId] = e;
+ _pointersCount++;
+}
+
+function _globalPointerMove(e) {
+ if (_pointers[e.pointerId]) {
+ _pointers[e.pointerId] = e;
+ }
+}
+
+function _globalPointerUp(e) {
+ delete _pointers[e.pointerId];
+ _pointersCount--;
+}
+
+function _handlePointer(e, handler) {
+ e.touches = [];
+ for (var i in _pointers) {
+ e.touches.push(_pointers[i]);
+ }
+ e.changedTouches = [e];
+
+ handler(e);
+}
+
+function _addPointerMove(obj, handler, id) {
+ var onMove = function (e) {
+ // don't fire touch moves when mouse isn't down
+ if ((e.pointerType === e.MSPOINTER_TYPE_MOUSE || e.pointerType === 'mouse') && e.buttons === 0) { return; }
+
+ _handlePointer(e, handler);
+ };
+
+ obj['_leaflet_touchmove' + id] = onMove;
+ obj.addEventListener(POINTER_MOVE, onMove, false);
+}
+
+function _addPointerEnd(obj, handler, id) {
+ var onUp = function (e) {
+ _handlePointer(e, handler);
+ };
+
+ obj['_leaflet_touchend' + id] = onUp;
+ obj.addEventListener(POINTER_UP, onUp, false);
+ obj.addEventListener(POINTER_CANCEL, onUp, false);
+}
+
+/*
+ * Extends the event handling code with double tap support for mobile browsers.
+ */
+
+var _touchstart = msPointer ? 'MSPointerDown' : pointer ? 'pointerdown' : 'touchstart';
+var _touchend = msPointer ? 'MSPointerUp' : pointer ? 'pointerup' : 'touchend';
+var _pre = '_leaflet_';
+
+// inspired by Zepto touch code by Thomas Fuchs
+function addDoubleTapListener(obj, handler, id) {
+ var last, touch$$1,
+ doubleTap = false,
+ delay = 250;
+
+ function onTouchStart(e) {
+ var count;
+
+ if (pointer) {
+ if ((!edge) || e.pointerType === 'mouse') { return; }
+ count = _pointersCount;
+ } else {
+ count = e.touches.length;
+ }
+
+ if (count > 1) { return; }
+
+ var now = Date.now(),
+ delta = now - (last || now);
+
+ touch$$1 = e.touches ? e.touches[0] : e;
+ doubleTap = (delta > 0 && delta <= delay);
+ last = now;
+ }
+
+ function onTouchEnd(e) {
+ if (doubleTap && !touch$$1.cancelBubble) {
+ if (pointer) {
+ if ((!edge) || e.pointerType === 'mouse') { return; }
+ // work around .type being readonly with MSPointer* events
+ var newTouch = {},
+ prop, i;
+
+ for (i in touch$$1) {
+ prop = touch$$1[i];
+ newTouch[i] = prop && prop.bind ? prop.bind(touch$$1) : prop;
+ }
+ touch$$1 = newTouch;
+ }
+ touch$$1.type = 'dblclick';
+ touch$$1.button = 0;
+ handler(touch$$1);
+ last = null;
+ }
+ }
+
+ obj[_pre + _touchstart + id] = onTouchStart;
+ obj[_pre + _touchend + id] = onTouchEnd;
+ obj[_pre + 'dblclick' + id] = handler;
+
+ obj.addEventListener(_touchstart, onTouchStart, passiveEvents ? {passive: false} : false);
+ obj.addEventListener(_touchend, onTouchEnd, passiveEvents ? {passive: false} : false);
+
+ // On some platforms (notably, chrome<55 on win10 + touchscreen + mouse),
+ // the browser doesn't fire touchend/pointerup events but does fire
+ // native dblclicks. See #4127.
+ // Edge 14 also fires native dblclicks, but only for pointerType mouse, see #5180.
+ obj.addEventListener('dblclick', handler, false);
+
+ return this;
+}
+
+function removeDoubleTapListener(obj, id) {
+ var touchstart = obj[_pre + _touchstart + id],
+ touchend = obj[_pre + _touchend + id],
+ dblclick = obj[_pre + 'dblclick' + id];
+
+ obj.removeEventListener(_touchstart, touchstart, passiveEvents ? {passive: false} : false);
+ obj.removeEventListener(_touchend, touchend, passiveEvents ? {passive: false} : false);
+ if (!edge) {
+ obj.removeEventListener('dblclick', dblclick, false);
+ }
+
+ return this;
+}
+
+/*
+ * @namespace DomUtil
+ *
+ * Utility functions to work with the [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model)
+ * tree, used by Leaflet internally.
+ *
+ * Most functions expecting or returning a `HTMLElement` also work for
+ * SVG elements. The only difference is that classes refer to CSS classes
+ * in HTML and SVG classes in SVG.
+ */
+
+
+// @property TRANSFORM: String
+// Vendor-prefixed transform style name (e.g. `'webkitTransform'` for WebKit).
+var TRANSFORM = testProp(
+ ['transform', 'webkitTransform', 'OTransform', 'MozTransform', 'msTransform']);
+
+// webkitTransition comes first because some browser versions that drop vendor prefix don't do
+// the same for the transitionend event, in particular the Android 4.1 stock browser
+
+// @property TRANSITION: String
+// Vendor-prefixed transition style name.
+var TRANSITION = testProp(
+ ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']);
+
+// @property TRANSITION_END: String
+// Vendor-prefixed transitionend event name.
+var TRANSITION_END =
+ TRANSITION === 'webkitTransition' || TRANSITION === 'OTransition' ? TRANSITION + 'End' : 'transitionend';
+
+
+// @function get(id: String|HTMLElement): HTMLElement
+// Returns an element given its DOM id, or returns the element itself
+// if it was passed directly.
+function get(id) {
+ return typeof id === 'string' ? document.getElementById(id) : id;
+}
+
+// @function getStyle(el: HTMLElement, styleAttrib: String): String
+// Returns the value for a certain style attribute on an element,
+// including computed values or values set through CSS.
+function getStyle(el, style) {
+ var value = el.style[style] || (el.currentStyle && el.currentStyle[style]);
+
+ if ((!value || value === 'auto') && document.defaultView) {
+ var css = document.defaultView.getComputedStyle(el, null);
+ value = css ? css[style] : null;
+ }
+ return value === 'auto' ? null : value;
+}
+
+// @function create(tagName: String, className?: String, container?: HTMLElement): HTMLElement
+// Creates an HTML element with `tagName`, sets its class to `className`, and optionally appends it to `container` element.
+function create$1(tagName, className, container) {
+ var el = document.createElement(tagName);
+ el.className = className || '';
+
+ if (container) {
+ container.appendChild(el);
+ }
+ return el;
+}
+
+// @function remove(el: HTMLElement)
+// Removes `el` from its parent element
+function remove(el) {
+ var parent = el.parentNode;
+ if (parent) {
+ parent.removeChild(el);
+ }
+}
+
+// @function empty(el: HTMLElement)
+// Removes all of `el`'s children elements from `el`
+function empty(el) {
+ while (el.firstChild) {
+ el.removeChild(el.firstChild);
+ }
+}
+
+// @function toFront(el: HTMLElement)
+// Makes `el` the last child of its parent, so it renders in front of the other children.
+function toFront(el) {
+ var parent = el.parentNode;
+ if (parent && parent.lastChild !== el) {
+ parent.appendChild(el);
+ }
+}
+
+// @function toBack(el: HTMLElement)
+// Makes `el` the first child of its parent, so it renders behind the other children.
+function toBack(el) {
+ var parent = el.parentNode;
+ if (parent && parent.firstChild !== el) {
+ parent.insertBefore(el, parent.firstChild);
+ }
+}
+
+// @function hasClass(el: HTMLElement, name: String): Boolean
+// Returns `true` if the element's class attribute contains `name`.
+function hasClass(el, name) {
+ if (el.classList !== undefined) {
+ return el.classList.contains(name);
+ }
+ var className = getClass(el);
+ return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className);
+}
+
+// @function addClass(el: HTMLElement, name: String)
+// Adds `name` to the element's class attribute.
+function addClass(el, name) {
+ if (el.classList !== undefined) {
+ var classes = splitWords(name);
+ for (var i = 0, len = classes.length; i < len; i++) {
+ el.classList.add(classes[i]);
+ }
+ } else if (!hasClass(el, name)) {
+ var className = getClass(el);
+ setClass(el, (className ? className + ' ' : '') + name);
+ }
+}
+
+// @function removeClass(el: HTMLElement, name: String)
+// Removes `name` from the element's class attribute.
+function removeClass(el, name) {
+ if (el.classList !== undefined) {
+ el.classList.remove(name);
+ } else {
+ setClass(el, trim((' ' + getClass(el) + ' ').replace(' ' + name + ' ', ' ')));
+ }
+}
+
+// @function setClass(el: HTMLElement, name: String)
+// Sets the element's class.
+function setClass(el, name) {
+ if (el.className.baseVal === undefined) {
+ el.className = name;
+ } else {
+ // in case of SVG element
+ el.className.baseVal = name;
+ }
+}
+
+// @function getClass(el: HTMLElement): String
+// Returns the element's class.
+function getClass(el) {
+ // Check if the element is an SVGElementInstance and use the correspondingElement instead
+ // (Required for linked SVG elements in IE11.)
+ if (el.correspondingElement) {
+ el = el.correspondingElement;
+ }
+ return el.className.baseVal === undefined ? el.className : el.className.baseVal;
+}
+
+// @function setOpacity(el: HTMLElement, opacity: Number)
+// Set the opacity of an element (including old IE support).
+// `opacity` must be a number from `0` to `1`.
+function setOpacity(el, value) {
+ if ('opacity' in el.style) {
+ el.style.opacity = value;
+ } else if ('filter' in el.style) {
+ _setOpacityIE(el, value);
+ }
+}
+
+function _setOpacityIE(el, value) {
+ var filter = false,
+ filterName = 'DXImageTransform.Microsoft.Alpha';
+
+ // filters collection throws an error if we try to retrieve a filter that doesn't exist
+ try {
+ filter = el.filters.item(filterName);
+ } catch (e) {
+ // don't set opacity to 1 if we haven't already set an opacity,
+ // it isn't needed and breaks transparent pngs.
+ if (value === 1) { return; }
+ }
+
+ value = Math.round(value * 100);
+
+ if (filter) {
+ filter.Enabled = (value !== 100);
+ filter.Opacity = value;
+ } else {
+ el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')';
+ }
+}
+
+// @function testProp(props: String[]): String|false
+// Goes through the array of style names and returns the first name
+// that is a valid style name for an element. If no such name is found,
+// it returns false. Useful for vendor-prefixed styles like `transform`.
+function testProp(props) {
+ var style = document.documentElement.style;
+
+ for (var i = 0; i < props.length; i++) {
+ if (props[i] in style) {
+ return props[i];
+ }
+ }
+ return false;
+}
+
+// @function setTransform(el: HTMLElement, offset: Point, scale?: Number)
+// Resets the 3D CSS transform of `el` so it is translated by `offset` pixels
+// and optionally scaled by `scale`. Does not have an effect if the
+// browser doesn't support 3D CSS transforms.
+function setTransform(el, offset, scale) {
+ var pos = offset || new Point(0, 0);
+
+ el.style[TRANSFORM] =
+ (ie3d ?
+ 'translate(' + pos.x + 'px,' + pos.y + 'px)' :
+ 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') +
+ (scale ? ' scale(' + scale + ')' : '');
+}
+
+// @function setPosition(el: HTMLElement, position: Point)
+// Sets the position of `el` to coordinates specified by `position`,
+// using CSS translate or top/left positioning depending on the browser
+// (used by Leaflet internally to position its layers).
+function setPosition(el, point) {
+
+ /*eslint-disable */
+ el._leaflet_pos = point;
+ /* eslint-enable */
+
+ if (any3d) {
+ setTransform(el, point);
+ } else {
+ el.style.left = point.x + 'px';
+ el.style.top = point.y + 'px';
+ }
+}
+
+// @function getPosition(el: HTMLElement): Point
+// Returns the coordinates of an element previously positioned with setPosition.
+function getPosition(el) {
+ // this method is only used for elements previously positioned using setPosition,
+ // so it's safe to cache the position for performance
+
+ return el._leaflet_pos || new Point(0, 0);
+}
+
+// @function disableTextSelection()
+// Prevents the user from generating `selectstart` DOM events, usually generated
+// when the user drags the mouse through a page with text. Used internally
+// by Leaflet to override the behaviour of any click-and-drag interaction on
+// the map. Affects drag interactions on the whole document.
+
+// @function enableTextSelection()
+// Cancels the effects of a previous [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection).
+var disableTextSelection;
+var enableTextSelection;
+var _userSelect;
+if ('onselectstart' in document) {
+ disableTextSelection = function () {
+ on(window, 'selectstart', preventDefault);
+ };
+ enableTextSelection = function () {
+ off(window, 'selectstart', preventDefault);
+ };
+} else {
+ var userSelectProperty = testProp(
+ ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']);
+
+ disableTextSelection = function () {
+ if (userSelectProperty) {
+ var style = document.documentElement.style;
+ _userSelect = style[userSelectProperty];
+ style[userSelectProperty] = 'none';
+ }
+ };
+ enableTextSelection = function () {
+ if (userSelectProperty) {
+ document.documentElement.style[userSelectProperty] = _userSelect;
+ _userSelect = undefined;
+ }
+ };
+}
+
+// @function disableImageDrag()
+// As [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection), but
+// for `dragstart` DOM events, usually generated when the user drags an image.
+function disableImageDrag() {
+ on(window, 'dragstart', preventDefault);
+}
+
+// @function enableImageDrag()
+// Cancels the effects of a previous [`L.DomUtil.disableImageDrag`](#domutil-disabletextselection).
+function enableImageDrag() {
+ off(window, 'dragstart', preventDefault);
+}
+
+var _outlineElement;
+var _outlineStyle;
+// @function preventOutline(el: HTMLElement)
+// Makes the [outline](https://developer.mozilla.org/docs/Web/CSS/outline)
+// of the element `el` invisible. Used internally by Leaflet to prevent
+// focusable elements from displaying an outline when the user performs a
+// drag interaction on them.
+function preventOutline(element) {
+ while (element.tabIndex === -1) {
+ element = element.parentNode;
+ }
+ if (!element.style) { return; }
+ restoreOutline();
+ _outlineElement = element;
+ _outlineStyle = element.style.outline;
+ element.style.outline = 'none';
+ on(window, 'keydown', restoreOutline);
+}
+
+// @function restoreOutline()
+// Cancels the effects of a previous [`L.DomUtil.preventOutline`]().
+function restoreOutline() {
+ if (!_outlineElement) { return; }
+ _outlineElement.style.outline = _outlineStyle;
+ _outlineElement = undefined;
+ _outlineStyle = undefined;
+ off(window, 'keydown', restoreOutline);
+}
+
+// @function getSizedParentNode(el: HTMLElement): HTMLElement
+// Finds the closest parent node which size (width and height) is not null.
+function getSizedParentNode(element) {
+ do {
+ element = element.parentNode;
+ } while ((!element.offsetWidth || !element.offsetHeight) && element !== document.body);
+ return element;
+}
+
+// @function getScale(el: HTMLElement): Object
+// Computes the CSS scale currently applied on the element.
+// Returns an object with `x` and `y` members as horizontal and vertical scales respectively,
+// and `boundingClientRect` as the result of [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect).
+function getScale(element) {
+ var rect = element.getBoundingClientRect(); // Read-only in old browsers.
+
+ return {
+ x: rect.width / element.offsetWidth || 1,
+ y: rect.height / element.offsetHeight || 1,
+ boundingClientRect: rect
+ };
+}
+
+
+var DomUtil = (Object.freeze || Object)({
+ TRANSFORM: TRANSFORM,
+ TRANSITION: TRANSITION,
+ TRANSITION_END: TRANSITION_END,
+ get: get,
+ getStyle: getStyle,
+ create: create$1,
+ remove: remove,
+ empty: empty,
+ toFront: toFront,
+ toBack: toBack,
+ hasClass: hasClass,
+ addClass: addClass,
+ removeClass: removeClass,
+ setClass: setClass,
+ getClass: getClass,
+ setOpacity: setOpacity,
+ testProp: testProp,
+ setTransform: setTransform,
+ setPosition: setPosition,
+ getPosition: getPosition,
+ disableTextSelection: disableTextSelection,
+ enableTextSelection: enableTextSelection,
+ disableImageDrag: disableImageDrag,
+ enableImageDrag: enableImageDrag,
+ preventOutline: preventOutline,
+ restoreOutline: restoreOutline,
+ getSizedParentNode: getSizedParentNode,
+ getScale: getScale
+});
+
+/*
+ * @namespace DomEvent
+ * Utility functions to work with the [DOM events](https://developer.mozilla.org/docs/Web/API/Event), used by Leaflet internally.
+ */
+
+// Inspired by John Resig, Dean Edwards and YUI addEvent implementations.
+
+// @function on(el: HTMLElement, types: String, fn: Function, context?: Object): this
+// Adds a listener function (`fn`) to a particular DOM event type of the
+// element `el`. You can optionally specify the context of the listener
+// (object the `this` keyword will point to). You can also pass several
+// space-separated types (e.g. `'click dblclick'`).
+
+// @alternative
+// @function on(el: HTMLElement, eventMap: Object, context?: Object): this
+// Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
+function on(obj, types, fn, context) {
+
+ if (typeof types === 'object') {
+ for (var type in types) {
+ addOne(obj, type, types[type], fn);
+ }
+ } else {
+ types = splitWords(types);
+
+ for (var i = 0, len = types.length; i < len; i++) {
+ addOne(obj, types[i], fn, context);
+ }
+ }
+
+ return this;
+}
+
+var eventsKey = '_leaflet_events';
+
+// @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this
+// Removes a previously added listener function.
+// Note that if you passed a custom context to on, you must pass the same
+// context to `off` in order to remove the listener.
+
+// @alternative
+// @function off(el: HTMLElement, eventMap: Object, context?: Object): this
+// Removes a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
+function off(obj, types, fn, context) {
+
+ if (typeof types === 'object') {
+ for (var type in types) {
+ removeOne(obj, type, types[type], fn);
+ }
+ } else if (types) {
+ types = splitWords(types);
+
+ for (var i = 0, len = types.length; i < len; i++) {
+ removeOne(obj, types[i], fn, context);
+ }
+ } else {
+ for (var j in obj[eventsKey]) {
+ removeOne(obj, j, obj[eventsKey][j]);
+ }
+ delete obj[eventsKey];
+ }
+
+ return this;
+}
+
+function addOne(obj, type, fn, context) {
+ var id = type + stamp(fn) + (context ? '_' + stamp(context) : '');
+
+ if (obj[eventsKey] && obj[eventsKey][id]) { return this; }
+
+ var handler = function (e) {
+ return fn.call(context || obj, e || window.event);
+ };
+
+ var originalHandler = handler;
+
+ if (pointer && type.indexOf('touch') === 0) {
+ // Needs DomEvent.Pointer.js
+ addPointerListener(obj, type, handler, id);
+
+ } else if (touch && (type === 'dblclick') && addDoubleTapListener &&
+ !(pointer && chrome)) {
+ // Chrome >55 does not need the synthetic dblclicks from addDoubleTapListener
+ // See #5180
+ addDoubleTapListener(obj, handler, id);
+
+ } else if ('addEventListener' in obj) {
+
+ if (type === 'mousewheel') {
+ obj.addEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, passiveEvents ? {passive: false} : false);
+
+ } else if ((type === 'mouseenter') || (type === 'mouseleave')) {
+ handler = function (e) {
+ e = e || window.event;
+ if (isExternalTarget(obj, e)) {
+ originalHandler(e);
+ }
+ };
+ obj.addEventListener(type === 'mouseenter' ? 'mouseover' : 'mouseout', handler, false);
+
+ } else {
+ if (type === 'click' && android) {
+ handler = function (e) {
+ filterClick(e, originalHandler);
+ };
+ }
+ obj.addEventListener(type, handler, false);
+ }
+
+ } else if ('attachEvent' in obj) {
+ obj.attachEvent('on' + type, handler);
+ }
+
+ obj[eventsKey] = obj[eventsKey] || {};
+ obj[eventsKey][id] = handler;
+}
+
+function removeOne(obj, type, fn, context) {
+
+ var id = type + stamp(fn) + (context ? '_' + stamp(context) : ''),
+ handler = obj[eventsKey] && obj[eventsKey][id];
+
+ if (!handler) { return this; }
+
+ if (pointer && type.indexOf('touch') === 0) {
+ removePointerListener(obj, type, id);
+
+ } else if (touch && (type === 'dblclick') && removeDoubleTapListener &&
+ !(pointer && chrome)) {
+ removeDoubleTapListener(obj, id);
+
+ } else if ('removeEventListener' in obj) {
+
+ if (type === 'mousewheel') {
+ obj.removeEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, passiveEvents ? {passive: false} : false);
+
+ } else {
+ obj.removeEventListener(
+ type === 'mouseenter' ? 'mouseover' :
+ type === 'mouseleave' ? 'mouseout' : type, handler, false);
+ }
+
+ } else if ('detachEvent' in obj) {
+ obj.detachEvent('on' + type, handler);
+ }
+
+ obj[eventsKey][id] = null;
+}
+
+// @function stopPropagation(ev: DOMEvent): this
+// Stop the given event from propagation to parent elements. Used inside the listener functions:
+// ```js
+// L.DomEvent.on(div, 'click', function (ev) {
+// L.DomEvent.stopPropagation(ev);
+// });
+// ```
+function stopPropagation(e) {
+
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ } else if (e.originalEvent) { // In case of Leaflet event.
+ e.originalEvent._stopped = true;
+ } else {
+ e.cancelBubble = true;
+ }
+ skipped(e);
+
+ return this;
+}
+
+// @function disableScrollPropagation(el: HTMLElement): this
+// Adds `stopPropagation` to the element's `'mousewheel'` events (plus browser variants).
+function disableScrollPropagation(el) {
+ addOne(el, 'mousewheel', stopPropagation);
+ return this;
+}
+
+// @function disableClickPropagation(el: HTMLElement): this
+// Adds `stopPropagation` to the element's `'click'`, `'doubleclick'`,
+// `'mousedown'` and `'touchstart'` events (plus browser variants).
+function disableClickPropagation(el) {
+ on(el, 'mousedown touchstart dblclick', stopPropagation);
+ addOne(el, 'click', fakeStop);
+ return this;
+}
+
+// @function preventDefault(ev: DOMEvent): this
+// Prevents the default action of the DOM Event `ev` from happening (such as
+// following a link in the href of the a element, or doing a POST request
+// with page reload when a `