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