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