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