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