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