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