1<?php 2 3use Fisharebest\Webtrees\I18N; 4use Fisharebest\Webtrees\View; 5 6/** 7 * @var array<mixed> $data 8 * @var array<mixed> $provider 9 */ 10?> 11 12<div class="py-4"> 13 <div class="row gchart osm-wrapper"> 14 <div id="osm-map" class="col-sm-9 wt-ajax-load osm-user-map" dir="ltr"></div> 15 <ul class="col-sm-3 osm-sidebar wt-page-options-value list-unstyled px-md-1"></ul> 16 </div> 17</div> 18 19<?php View::push('styles') ?> 20<style> 21 .osm-wrapper, .osm-user-map { 22 height: 70vh 23 } 24 25 .osm-sidebar { 26 height: 100%; 27 overflow-y: auto; 28 font-size: small; 29 } 30 31</style> 32<?php View::endpush() ?> 33 34<?php View::push('javascript') ?> 35<script> 36'use strict'; 37 38window.WT_OSM = (function () { 39 const minZoom = 2; 40 41 let map = null; 42 let zoom = null; 43 let sidebar = $('.osm-sidebar'); 44 let provider = <?= json_encode($provider) ?>; 45 46// Map components 47 let markers = L.markerClusterGroup({ 48 showCoverageOnHover: false, 49 }); 50 51 let resetControl = L.Control.extend({ 52 options: { 53 position: 'topleft', 54 }, 55 onAdd: function (map) { 56 let container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-custom'); 57 container.onclick = function () { 58 if (zoom) { 59 map.flyTo(markers.getBounds().getCenter(), zoom); 60 } else { 61 map.flyToBounds(markers.getBounds().pad(0.2)); 62 } 63 sidebar.scrollTo(sidebar.children(':first')); 64 65 return false; 66 }; 67 let reset = <?= json_encode(I18N::translate('Reload map')) ?>; 68 let anchor = L.DomUtil.create('a', 'leaflet-control-reset', container); 69 anchor.setAttribute('aria-label', reset); 70 anchor.href = '#'; 71 anchor.title = reset; 72 anchor.role = 'button'; 73 let image = L.DomUtil.create('i', 'fas fa-redo', anchor); 74 image.alt = reset; 75 76 return container; 77 }, 78 }); 79 80// Zoom control with localised text 81 let newZoomControl = new L.control.zoom({ 82 zoomInTitle: <?= json_encode(I18N::translate('Zoom in')) ?>, 83 zoomOutTitle: <?= json_encode(I18N::translate('Zoom out')) ?>, 84 }); 85 86 /** 87 * 88 * @private 89 */ 90 let _drawMap = function () { 91 map = L.map('osm-map', { 92 center: [0, 0], 93 minZoom: minZoom, // maxZoom set by leaflet-providers.js 94 zoomControl: false, // remove default 95 }) 96 .addControl(new resetControl()) 97 .addControl(newZoomControl) 98 .addLayer(L.tileLayer(provider.url, provider.options)); 99 }; 100 101 /** 102 * @private 103 */ 104 let _buildMapData = function () { 105 let sidebar_content = ''; 106 let geoJson_data = <?= json_encode($data) ?>; 107 108 if (geoJson_data.features.length === 0) { 109 map.fitWorld(); 110 sidebar_content += '<div class="bg-info text-white text-center">' + <?= json_encode(I18N::translate('Nothing to show')) ?> +'</div>'; 111 } else { 112 if (geoJson_data.features.length === 1) { 113 //fudge factor - maps zooms to maximum permitted otherwise 114 zoom = geoJson_data.features[0].properties.zoom; 115 } 116 let geoJsonLayer = L.geoJson(geoJson_data, { 117 pointToLayer: function (feature, latlng) { 118 return new L.Marker(latlng, { 119 icon: L.BeautifyIcon.icon({ 120 icon: 'bullseye fas', 121 borderColor: 'transparent', 122 backgroundColor: feature.properties.iconcolor, 123 iconShape: 'marker', 124 textColor: 'white', 125 }), 126 title: feature.properties.tooltip, 127 alt: feature.properties.tooltip, 128 id: feature.id, 129 }) 130 .on('popupopen', function (e) { 131 let item = sidebar.children('.gchart[data-id=' + e.target.feature.id + ']'); 132 item.addClass('messagebox'); 133 sidebar.scrollTo(item); 134 }) 135 .on('popupclose', function () { 136 sidebar.children('.gchart').removeClass('messagebox'); 137 }); 138 }, 139 onEachFeature: function (feature, layer) { 140 if (feature.properties.polyline) { 141 let pline = L.polyline(feature.properties.polyline.points, feature.properties.polyline.options); 142 markers.addLayer(pline); 143 } 144 layer.bindPopup(feature.properties.summary); 145 sidebar_content += `<li class="gchart px-md-2" data-id=${feature.id}>${feature.properties.summary}</li>`; 146 }, 147 }); 148 markers.addLayer(geoJsonLayer); 149 map.addLayer(markers); 150 if (zoom) { 151 map.setView(markers.getBounds().getCenter(), zoom); 152 } else { 153 map.fitBounds(markers.getBounds(), {padding: [50, 30]}); 154 } 155 } 156 sidebar.append(sidebar_content); 157 }; 158 159 /** 160 * @param elem 161 * @returns {$} 162 */ 163 $.fn.scrollTo = function (elem) { 164 let _this = $(this); 165 _this.animate({ 166 scrollTop: elem.offset().top - _this.offset().top + _this.scrollTop(), 167 }); 168 return this; 169 }; 170 171// Activate marker popup when sidebar entry clicked 172 $(function () { 173 sidebar 174 // open marker popup if sidebar event is clicked 175 .on('click', '.gchart', function (e) { 176 // first close any existing 177 map.closePopup(); 178 let eventId = $(this).data('id'); 179 //find the marker corresponding to the clicked event 180 let mkrLayer = markers.getLayers().filter(function (v) { 181 return typeof (v.feature) !== 'undefined' && v.feature.id === eventId; 182 }); 183 let mkr = mkrLayer.pop(); 184 // Unfortunately zoomToShowLayer zooms to maxZoom 185 // when all marker in a cluster have exactly the 186 // same co-ordinates 187 markers.zoomToShowLayer(mkr, function (e) { 188 mkr.openPopup(); 189 }); 190 191 return false; 192 }) 193 .on('click', 'a', function (e) { // stop click on a person also opening the popup 194 e.stopPropagation(); 195 }); 196 }); 197 198 _drawMap(); 199 _buildMapData(); 200 201 return 'Leaflet map interface for webtrees-2'; 202})(); 203</script> 204<?php View::endpush() ?> 205