1<?php 2use Fisharebest\Webtrees\I18N; 3use Fisharebest\Webtrees\Individual; 4use Fisharebest\Webtrees\View; 5 6/** 7 * @var Individual $individual 8 */ 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 p-0"></ul> 16 </div> 17</div> 18 19<?php View::push('styles') ?> 20<style> 21 .osm-wrapper, .osm-user-map { 22 height: 75vh 23 } 24 25 .osm-sidebar { 26 height: 100%; 27 overflow-y: auto; 28 padding: 0; 29 margin: 0; 30 border: 0; 31 display: none; 32 font-size: small; 33 } 34 35 .osm-sidebar .gchart { 36 margin: 1px; 37 padding: 2px 38 } 39 40 .osm-sidebar .gchart img { 41 height: 15px; 42 width: 25px 43 } 44 45 .osm-sidebar .border-danger:hover { 46 cursor: not-allowed 47 } 48</style> 49<?php View::endpush() ?> 50 51<?php View::push('javascript') ?> 52<script type="application/javascript"> 53 "use strict"; 54 55 window.WT_OSM = (function() { 56 let baseData = { 57 minZoom: 2, 58 providerName: "OpenStreetMap.Mapnik", 59 providerOptions: [], 60 I18N: { 61 zoomInTitle: <?= json_encode(I18N::translate('Zoom in')) ?>, 62 zoomOutTitle: <?= json_encode(I18N::translate('Zoom out')) ?>, 63 reset: <?= json_encode(I18N::translate('Reset to initial map state')) ?>, 64 noData: <?= json_encode(I18N::translate('No mappable items')) ?>, 65 error: <?= json_encode(I18N::translate('An unknown error occurred')) ?> 66 } 67 }; 68 69 let map = null; 70 let zoom = null; 71 let markers = L.markerClusterGroup({ 72 showCoverageOnHover: false 73 }); 74 75 let resetControl = L.Control.extend({ 76 options: { 77 position: 'topleft' 78 }, 79 80 onAdd: function (map) { 81 let container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-custom'); 82 container.onclick = function () { 83 if (zoom) { 84 map.flyTo(markers.getBounds().getCenter(), zoom); 85 } else { 86 map.flyToBounds(markers.getBounds().pad(0.2)); 87 } 88 return false; 89 }; 90 let anchor = L.DomUtil.create('a', 'leaflet-control-reset', container); 91 anchor.href = '#'; 92 anchor.title = baseData.I18N.reset; 93 anchor.role = 'button'; 94 $(anchor).attr('aria-label', 'reset'); 95 let image = L.DomUtil.create('i', 'fas fa-redo', anchor); 96 image.alt = baseData.I18N.reset; 97 98 return container; 99 }, 100 }); 101 102 /** 103 * 104 * @private 105 */ 106 let _drawMap = function () { 107 map = L.map('osm-map', { 108 center : [0, 0], 109 minZoom : baseData.minZoom, // maxZoom set by leaflet-providers.js 110 zoomControl: false, // remove default 111 }); 112 L.tileLayer.provider(baseData.providerName, baseData.providerOptions).addTo(map); 113 L.control.zoom({ // Add zoom with localised text 114 zoomInTitle : baseData.I18N.zoomInTitle, 115 zoomOutTitle: baseData.I18N.zoomOutTitle, 116 }).addTo(map); 117 }; 118 119 /** 120 * @param Generations 121 * @private 122 */ 123 let _addLayer = function (Generations) { 124 let geoJsonLayer; 125 let domObj = '.osm-sidebar'; 126 let sidebar = ''; 127 128 $.getJSON(<?= json_encode(route('module', ['module' => 'pedigree-map', 'action' => 'MapData', 'tree' => $individual->tree()->name(), 'xref' => $individual->xref()])) ?>, { 129 generations: Generations 130 }) 131 .done(function (data, textStatus, jqXHR) { 132 if (jqXHR.status === 200 && data.features.length === 1) { 133 zoom = data.features[0].properties.zoom; 134 } 135 geoJsonLayer = L.geoJson(data, { 136 pointToLayer : function (feature, latlng) { 137 return new L.Marker(latlng, { 138 icon : L.BeautifyIcon.icon({ 139 icon : feature.properties.icon['name'], 140 borderColor : 'transparent', 141 backgroundColor: feature.valid ? feature.properties.icon['color'] : 'transparent', 142 iconShape : 'marker', 143 textColor : feature.valid ? 'white' : 'transparent', 144 }), 145 title: feature.properties.tooltip, 146 alt : feature.properties.tooltip, 147 id : feature.id 148 }) 149 .on('popupopen', function (e) { 150 let sidebar = $('.osm-sidebar'); 151 let item = sidebar.children(".gchart[data-id=" + e.target.feature.id + "]"); 152 item.addClass('messagebox'); 153 sidebar.scrollTo(item); 154 }) 155 .on('popupclose', function () { 156 $('.osm-sidebar').children(".gchart") 157 .removeClass('messagebox'); 158 }); 159 }, 160 onEachFeature: function (feature, layer) { 161 if (feature.properties.polyline) { 162 let pline = L.polyline(feature.properties.polyline.points, feature.properties.polyline.options); 163 markers.addLayer(pline); 164 } 165 layer.bindPopup(feature.properties.summary); 166 let myclass = feature.valid ? 'gchart' : 'border border-danger'; 167 sidebar += `<li class="${myclass}" data-id=${feature.id}>${feature.properties.summary}</li>`; 168 }, 169 }); 170 }) 171 .fail(function (jqXHR, textStatus, errorThrown) { 172 console.log(jqXHR, textStatus, errorThrown); 173 }) 174 .always(function (data_jqXHR, textStatus, jqXHR_errorThrown) { 175 switch (jqXHR_errorThrown.status) { 176 case 200: // Success 177 $(domObj).append(sidebar); 178 markers.addLayer(geoJsonLayer); 179 map 180 .addControl(new resetControl()) 181 .addLayer(markers) 182 .fitBounds(markers.getBounds().pad(0.2)); 183 if (zoom) { 184 map.setView(markers.getBounds().getCenter(), zoom); 185 } 186 break; 187 case 204: // No data 188 map.fitWorld(); 189 $(domObj).append('<div class="bg-info text-white">' + baseData.I18N.noData + '</div>'); 190 break; 191 default: // Anything else 192 map.fitWorld(); 193 $(domObj).append('<div class="bg-danger text-white">' + baseData.I18N.error + '</div>'); 194 } 195 $(domObj).slideDown(300); 196 }); 197 }; 198 199 /** 200 * 201 * @param elem 202 * @returns {$} 203 */ 204 205 $.fn.scrollTo = function (elem) { 206 let _this = $(this); 207 _this.animate({ 208 scrollTop: elem.offset().top - _this.offset().top + _this.scrollTop() 209 }); 210 return this; 211 }; 212 213 /** 214 * @param generations integer 215 * @private 216 */ 217 let _initialize = function (generations) { 218 // Activate marker popup when sidebar entry clicked 219 $(function () { 220 $('.osm-sidebar') 221 // open marker popup if sidebar event is clicked 222 .on('click', '.gchart', function (e) { 223 // first close any existing 224 map.closePopup(); 225 let eventId = $(this).data('id'); 226 //find the marker corresponding to the clicked event 227 let mkrLayer = markers.getLayers().filter(function (v) { 228 return typeof(v.feature) !== 'undefined' && v.feature.id === eventId; 229 }); 230 let mkr = mkrLayer.pop(); 231 // Unfortunately zoomToShowLayer zooms to maxZoom 232 // when all marker in a cluster have exactly the 233 // same co-ordinates 234 markers.zoomToShowLayer(mkr, function (e) { 235 mkr.openPopup(); 236 }); 237 return false; 238 }) 239 .on('click', 'a', function (e) { // stop click on a person also opening the popup 240 e.stopPropagation(); 241 }); 242 }); 243 244 _drawMap(); 245 _addLayer(generations); 246 }; 247 248 return { 249 /** 250 * @param generations integer 251 */ 252 drawMap: function (generations) { 253 _initialize(generations); 254 } 255 }; 256 })(); 257 258 WT_OSM.drawMap(<?= json_encode($generations) ?>); 259</script> 260<?php View::endpush() ?> 261