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