From 9da8f705f68eb45ca83fa040c58636fd2b0e1f6d Mon Sep 17 00:00:00 2001 From: SNoiraud Date: Sun, 7 Jul 2019 19:25:42 +0200 Subject: [PATCH] Narrative web: Add popup to manage markers If you click on a marker in the family map page, you get a popup. In this popup associated with a place, if you have several events, for each event you see the person and the event type. If you click on the person, you go to the related page for this person. If you click on the event type, you go to the related page for this event. Fixes #11150 --- data/css/narrative-maps.css | 45 ++++++ gramps/plugins/webreport/basepage.py | 4 +- gramps/plugins/webreport/common.py | 153 +++++++++++++------ gramps/plugins/webreport/person.py | 219 +++++++++++++++++++-------- gramps/plugins/webreport/place.py | 6 +- 5 files changed, 316 insertions(+), 111 deletions(-) diff --git a/data/css/narrative-maps.css b/data/css/narrative-maps.css index ae926e390..0fb9d1007 100644 --- a/data/css/narrative-maps.css +++ b/data/css/narrative-maps.css @@ -66,3 +66,48 @@ div#FamilyMapDetail div#references table.infolist { div#FamilyMapDetail div#references table.infolist tbody tr td.ColumnPlace { width: 40%; } + + +/* Subsection: popup +------------------------------------------------------ */ +.ol-popup { + position: absolute; + background-color: white; + -webkit-filter: drop-shadow(0 1px 4px rgba(0,0,0,0.2)); + filter: drop-shadow(0 1px 4px rgba(0,0,0,0.2)); + padding: 15px; + border-radius: 10px; + border: 2px solid #111111; + bottom: 12px; + left: -50px; + min-width: 450px; +} +.ol-popup:after, .ol-popup:before { + top: 100%; + border: solid transparent; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} +.ol-popup:after { + border-top-color: white; + border-width: 10px; + left: 48px; + margin-left: -10px; +} +.ol-popup:before { + border-top-color: #cccccc; + border-width: 11px; + left: 48px; + margin-left: -11px; +} +.ol-popup-closer { + text-decoration: none; + position: absolute; + top: 2px; + right: 8px; +} +.ol-popup-closer:after { + content: "✖"; +} diff --git a/gramps/plugins/webreport/basepage.py b/gramps/plugins/webreport/basepage.py index c7c3ac4f9..831a59ead 100644 --- a/gramps/plugins/webreport/basepage.py +++ b/gramps/plugins/webreport/basepage.py @@ -742,7 +742,7 @@ class BasePage: # pylint: disable=C1001 event_date = event.get_date_object() # 0 = latitude, 1 = longitude, 2 - placetitle, - # 3 = place handle, 4 = event date, 5 = event type + # 3 = place handle, 4 = event found = any(data[3] == place_handle and data[4] == event_date for data in place_lat_long) if not found: @@ -754,7 +754,7 @@ class BasePage: # pylint: disable=C1001 if latitude is not None: etype = event.get_type() place_lat_long.append([latitude, longitude, placetitle, - place_handle, event_date, etype]) + place_handle, event]) def _get_event_place(self, person, place_lat_long): """ diff --git a/gramps/plugins/webreport/common.py b/gramps/plugins/webreport/common.py index 1f733f940..324143e1e 100644 --- a/gramps/plugins/webreport/common.py +++ b/gramps/plugins/webreport/common.py @@ -120,16 +120,27 @@ DROPMASTERS = """ function addMarker() { var location = tracelife[iterator]; var myLatLng = new google.maps.LatLng(location[1], location[2]); + var infoWindow = new google.maps.InfoWindow; - markers.push(new google.maps.Marker({ + var marker = new google.maps.Marker({ position: myLatLng, map: map, draggable: true, title: location[0], animation: google.maps.Animation.DROP - })); + }); + markers.push(marker); iterator++; - }""" + var title = "

" + location[0] + "

" + bindInfoWindow(marker, map, infoWindow, title+location[4]); + } + function bindInfoWindow(marker, map, infoWindow, html) { + google.maps.event.addListener(marker, 'click', function() { + infoWindow.setContent(html); + infoWindow.open(map, marker); + }); + } +""" # javascript for Google's Markers... MARKERS = """ @@ -153,6 +164,7 @@ MARKERS = """ function addMarkers() { var bounds = new google.maps.LatLngBounds(); + var infoWindow = new google.maps.InfoWindow; for (var i = 0; i < tracelife.length; i++) { var location = tracelife[i]; @@ -165,10 +177,20 @@ MARKERS = """ map: map, zIndex: location[3] }); + var title = "

" + location[0] + "

" + bindInfoWindow(marker, map, infoWindow, title+location[4]); bounds.extend(myLatLng); if ( i > 1 ) { map.fitBounds(bounds); }; } - }""" + } + function bindInfoWindow(marker, map, infoWindow, html) { + google.maps.event.addListener(marker, 'click', function() { + infoWindow.setContent(html); + infoWindow.open(map, marker); + }); + } + +""" # javascript for OpenStreetMap's markers... """ @@ -178,7 +200,7 @@ https://openlayers.org/en/latest/examples/ OSM_MARKERS = """ function initialize(){ var map; - var tracelife = %s; + var tracelife = %s var iconStyle = new ol.style.Style({ image: new ol.style.Icon(({ opacity: 1.0, @@ -193,6 +215,7 @@ OSM_MARKERS = """ geometry: new ol.geom.Point(ol.proj.transform([loc[0], loc[1]], 'EPSG:4326', 'EPSG:3857')), name: loc[2], + data: loc[3], }); iconFeature.setStyle(iconStyle); markerSource.addFeature(iconFeature); @@ -201,11 +224,15 @@ OSM_MARKERS = """ source: markerSource, style: iconStyle }); + tooltip = new ol.layer.Vector({ + source: markerSource, + style: iconStyle + }); var centerCoord = new ol.proj.transform([%s, %s], 'EPSG:4326', 'EPSG:3857'); map = new ol.Map({ target: 'map_canvas', layers: [new ol.layer.Tile({ source: new ol.source.OSM() }), - markerLayer], + markerLayer, tooltip], view: new ol.View({ center: centerCoord, zoom: %d }) }); """ @@ -213,85 +240,119 @@ OSM_MARKERS = """ STAMEN_MARKERS = """ function initialize(){ var map; - var tracelife = %s; + var tracelife = %s var layer = '%s'; var iconStyle = new ol.style.Style({ image: new ol.style.Icon(({ - //anchor: [0.5, 46], + anchor: [0.2, 48], anchorXUnits: 'fraction', anchorYUnits: 'pixels', opacity: 1.0, src: marker_png })) }); - var markerSource = new ol.Collection(); + var markerSource = new ol.source.Vector({ + }); for (var i = 0; i < tracelife.length; i++) { var loc = tracelife[i]; var iconFeature = new ol.Feature({ geometry: new ol.geom.Point(ol.proj.transform([loc[0], loc[1]], 'EPSG:4326', 'EPSG:3857')), name: loc[2], + data: loc[3], }); iconFeature.setStyle(iconStyle); - markerSource.push(iconFeature); + markerSource.addFeature(iconFeature); } var centerCoord = new ol.proj.transform([%s, %s], 'EPSG:4326', 'EPSG:3857'); + markerLayer = new ol.layer.Vector({ + source: markerSource, + style: iconStyle + }); + tooltip = new ol.layer.Vector({ + source: markerSource, + style: iconStyle + }); map = new ol.Map({ target: 'map_canvas', layers: [ new ol.layer.Tile({ source: new ol.source.Stamen({ - layer: layer - }) - }), - new ol.layer.Vector({ source: new ol.source.Vector({ - features: markerSource }) - }) - ], + layer: layer }) }), + markerLayer, tooltip], view: new ol.View({ center: centerCoord, zoom: %d }) }); """ OPENLAYER = """ var element = document.getElementById('popup'); + var content = document.getElementById('popup-content'); + var closer = document.getElementById('popup-closer'); + var tip = document.getElementById('tooltip'); + var tipcontent = document.getElementById('tooltip-content'); + var tooltip = new ol.Overlay({ - element: element, + element: tip, positioning: 'bottom-center', - stopEvent: false + offset: [10, 0], }); map.addOverlay(tooltip); - var displayFeatureInfo = function(pixel) { - var feature = map.forEachFeatureAtPixel(pixel, function(feature, layer) { + + var popup = new ol.Overlay({ + element: element, + positioning: 'bottom-center', + autoPan: true, + autoPanAnimation: { duration: 500 }, + stopEvent: false + }); + map.addOverlay(popup); + + /** + * Add a click handler to hide the popup. + * @return {boolean} Don't follow the href. + */ + closer.onclick = function() { + popup.setPosition(undefined); + closer.blur(); + return false; + }; + + map.on('pointermove', function(evt) { + evt.preventDefault() + var feature = this.forEachFeatureAtPixel(evt.pixel, + function(feature, layer) { return feature; }); - var info = document.getElementById('popup'); - if (feature) { - var geometry = feature.getGeometry(); - var coord = geometry.getCoordinates(); - tooltip.setPosition(coord); - $(element).siblings('.popover').css({ width: '250px' }); - $(element).siblings('.popover').css({ background: '#aaa' }); - $(info).popover({ - 'placement': 'auto', - 'html': true, - 'content': feature.get('name') - }); - $(info).popover('show'); - } else { - // TODO : some warning with firebug here - $(info).popover('destroy'); - $('.popover').remove(); - } - }; - map.on('pointermove', function(evt) { + map.getTargetElement().style.cursor = feature ? 'pointer' : ''; if (evt.dragging) { + popup.setPosition(undefined); + tooltip.setPosition(undefined); return; } - var pixel = map.getEventPixel(evt.originalEvent); - displayFeatureInfo(pixel); - }); - map.on('click', function(evt) { - displayFeatureInfo(evt.pixel); + if (feature) { + var coordinate = evt.coordinate; + tipcontent.innerHTML = feature.get('name'); + tooltip.setPosition(coordinate); + } else { + tooltip.setPosition(undefined); + } + }); + map.on('singleclick', function(evt) { + evt.preventDefault() + var feature = map.forEachFeatureAtPixel(evt.pixel, + function(feature, layer) { + return feature; + }); + if (feature) { + var coordinate = evt.coordinate; + var title = '

' + feature.get('name') + '

'; + content.innerHTML = title + feature.get('data'); + popup.setPosition(coordinate); + } else { + popup.setPosition(undefined); + } + }); + }; """ diff --git a/gramps/plugins/webreport/person.py b/gramps/plugins/webreport/person.py index 450d0afc2..3016b2c67 100644 --- a/gramps/plugins/webreport/person.py +++ b/gramps/plugins/webreport/person.py @@ -52,7 +52,7 @@ import logging #------------------------------------------------ from gramps.gen.const import GRAMPS_LOCALE as glocale from gramps.gen.lib import (ChildRefType, Date, Name, Person, EventRoleType, - EventType) + Event, EventType) from gramps.gen.lib.date import Today from gramps.gen.plug.report import Bibliography from gramps.gen.plug.report import utils @@ -121,6 +121,7 @@ class PersonPages(BasePage): self.sort_name = None self.googleopts = None self.googlemapkey = None + self.stamenopts = None self.birthorder = None self.person = None self.familymappages = None @@ -617,6 +618,35 @@ class PersonPages(BasePage): # and close the file self.xhtml_writer(indivdetpage, output_file, sio, date) + def _create_family_tracelife(self, tracelife, placetitle, + latitude, longitude, seq_, links): + """ + creates individual family tracelife map events + + @param: person -- person from database + @param: links -- used to add links in the popup html page + """ + # are we using Google? + if self.mapservice == "Google": + + # are we creating Family Links? + if self.googleopts == "FamilyLinks": + tracelife += """ + new google.maps.LatLng(%s, %s),""" % (latitude, longitude) + + # are we creating Drop Markers or Markers? + elif self.googleopts in ["Drop", "Markers"]: + tracelife += """ + ['%s', %s, %s, %d, %s],""" % (placetitle.replace("'", "\\'"), latitude, + longitude, seq_, links) + + # are we using OpenStreetMap, Stamen... + else: + tracelife += """ + [%f, %f, \'%s\', %s],""" % (float(longitude), float(latitude), + placetitle.replace("'", "\\'"), links) + return tracelife + def __create_family_map(self, person, place_lat_long): """ creates individual family map page @@ -640,7 +670,7 @@ class PersonPages(BasePage): number_markers = len(place_lat_long) if number_markers > 1: for (latitude, longitude, placetitle, handle, - date, etype) in place_lat_long: + event) in place_lat_long: xwidth.append(latitude) yheight.append(longitude) xwidth.sort() @@ -680,8 +710,8 @@ class PersonPages(BasePage): # 0 = latitude, 1 = longitude, 2 = place title, # 3 = handle, and 4 = date, 5 = event type... - # being sorted by date, latitude, and longitude... - place_lat_long = sorted(place_lat_long, key=itemgetter(4, 0, 1)) + # being sorted by place_title + place_lat_long = sorted(place_lat_long, key=itemgetter(2)) # for all plugins # if family_detail_page @@ -730,61 +760,113 @@ class PersonPages(BasePage): if number_markers > 0: tracelife = "[" - seq_ = 1 + seq_ = 0 - for index in range(0, (number_markers - 1)): - (latitude, longitude, placetitle, handle, date, - etype) = place_lat_long[index] + old_place_title = "" + oldevent = None + links = "" + ln_str = "%s" + for index in range(0, number_markers): + (latitude, longitude, placetitle, handle, + event) = place_lat_long[index] + # Do we have several events for this place? + if placetitle == old_place_title: + evthdle = event.get_handle() + bkref_list = self.report.bkref_dict[Event][evthdle] + url_fct = self.report.build_url_fname_html + if bkref_list: + for ref in bkref_list: + (bkref_class, bkref_hdle, role) = ref + if role == "Primary": + url = url_fct(bkref_hdle, + "ppl", self.uplink) + ppl_fct = self.r_db.get_person_from_handle + person = ppl_fct(bkref_hdle) + ppl_lnk = ln_str % (url, + person.get_gramps_id(), + self.get_name(person)) + url = self.report.build_url_fname_html(event.get_handle(), + "evt", self.uplink) + evt_type = self._(str(event.get_type())) + evt_date = self.rlocale.get_date(event.get_date_object()) + evt_lnk = ln_str % (url, evt_date, evt_type) - # are we using Google? - if self.mapservice == "Google": + links += ' + "
%s"' % (ppl_lnk + self._(":") + evt_lnk) + if index == number_markers - 1: + tracelife = self._create_family_tracelife(tracelife, + placetitle, + latitude, + longitude, + seq_, + links) + break + continue + elif old_place_title != "" and index != 0: + (lat, lng, plcetitle, handle_, + event_) = place_lat_long[index-1] + tracelife = self._create_family_tracelife(tracelife, + plcetitle, + lat, + lng, + seq_, + links) + if old_place_title != placetitle: + old_place_title = placetitle + evthdle = event.get_handle() + bkref_list = self.report.bkref_dict[Event][evthdle] + url_fct = self.report.build_url_fname_html + if bkref_list: + for ref in bkref_list: + (bkref_class, bkref_hdle, role) = ref + if role == "Primary": + url = url_fct(bkref_hdle, + "ppl", self.uplink) + ppl_fct = self.r_db.get_person_from_handle + person = ppl_fct(bkref_hdle) + ppl_lnk = ln_str % (url, + person.get_gramps_id(), + self.get_name(person)) + url = self.report.build_url_fname_html(event.handle, + "evt", + self.uplink) + evt_type = self._(str(event.get_type())) + evt_date = self.rlocale.get_date(event.get_date_object()) + evt_lnk = ln_str % (url, evt_date, evt_type) - # are we creating Family Links? - if self.googleopts == "FamilyLinks": - tracelife += """ - new google.maps.LatLng(%s, %s),""" % (latitude, longitude) - - # are we creating Drop Markers or Markers? - elif self.googleopts in ["Drop", "Markers"]: - tracelife += """ - ['%s', %s, %s, %d],""" % (placetitle.replace("'", "\\'"), latitude, - longitude, seq_) - - # are we using OpenStreetMap? + links = '"
%s"' % (ppl_lnk + self._(":") + + evt_lnk) + elif index == number_markers: + tracelife = self._create_family_tracelife(tracelife, + placetitle, + latitude, + longitude, + seq_, + links) else: - tracelife += """ - [%f, %f, \'%s\'],""" % (float(longitude), float(latitude), - placetitle.replace("'", "\\'")) - + evthdle = event.get_handle() + bkref_list = self.report.bkref_dict[Event][evthdle] + url_fct = self.report.build_url_fname_html + if bkref_list: + for ref in bkref_list: + (bkref_class, bkref_hdle, role) = ref + if role == "Primary": + url = url_fct(bkref_hdle, + "ppl", self.uplink) + ppl_fct = self.r_db.get_person_from_handle + person = ppl_fct(bkref_hdle) + ppl_lnk = ln_str % (url, + person.get_gramps_id(), + self.get_name(person)) + url = self.report.build_url_fname_html(event.handle, + "evt", + self.uplink) + evt_type = self._(str(event.get_type())) + evt_lnk = ln_str % (url, evt_type, evt_type) + links = '"

%s"' % (ppl_lnk + self._(":") + evt_lnk) + old_place_title = placetitle seq_ += 1 - # FIXME: The last element in the place_lat_long list is treated - # specially, and the code above is apparently repeated so as to - # avoid a comma at the end, and get the right closing. This is very - # ugly. - (latitude, longitude, placetitle, handle, date, - etype) = place_lat_long[-1] - - # are we using Google? - if self.mapservice == "Google": - - # are we creating Family Links? - if self.googleopts == "FamilyLinks": - tracelife += """ - new google.maps.LatLng(%s, %s) - ];""" % (latitude, longitude) - - # are we creating Drop Markers or Markers? - elif self.googleopts in ["Drop", "Markers"]: - tracelife += """ - ['%s', %s, %s, %d] - ];""" % (placetitle.replace("'", "\\'"), latitude, longitude, seq_) - - # we are using OpenStreetMap, Stamen... - else: - tracelife += """ - [%f, %f, \'%s\'] - ];""" % (float(longitude), float(latitude), placetitle.replace("'", "\\'")) + tracelife += "];" # begin MapDetail division... with Html("div", class_="content", id="FamilyMapDetail") as mapdetail: outerwrapper += mapdetail @@ -872,13 +954,13 @@ class PersonPages(BasePage): longitude, latitude, 10, - ) + ) else: jsc += STAMEN_MARKERS % (tracelife, self.stamenopts, midy_, midx_, zoomlevel, - ) + ) jsc += OPENLAYER # if Google and Drop Markers are selected, @@ -888,8 +970,20 @@ class PersonPages(BasePage): id="drop", onclick="drop()", inline=True) # add div for popups. - with Html("div", id="popup", inline=True) as popup: - mapdetail += popup + if self.mapservice == "Google": + with Html("div", id="popup", inline=True) as popup: + mapdetail += popup + else: + with Html("div", id="popup", class_="ol-popup", + inline=True) as popup: + mapdetail += popup + popup += Html("a", href="#", id="popup-closer", + class_="ol-popup-closer") + popup += Html("div", id="popup-content") + with Html("div", id="tooltip", class_="ol-popup", + inline=True) as tooltip: + mapdetail += tooltip + tooltip += Html("div", id="tooltip-content") # begin place reference section and its table... with Html("div", class_="subsection", id="references") as section: @@ -917,11 +1011,16 @@ class PersonPages(BasePage): tbody = Html("tbody") table += tbody - for (latitude, longitude, placetitle, handle, date, - etype) in place_lat_long: + # being sorted by date + place_lat_long = sorted(place_lat_long, + key=lambda evt: + evt[4].get_date_object()) + for (latitude, longitude, placetitle, handle, + event) in place_lat_long: trow = Html("tr") tbody += trow + date = event.get_date_object() trow.extend( Html("td", data, class_=colclass, inline=True) for data, colclass in [ @@ -929,7 +1028,7 @@ class PersonPages(BasePage): (self.place_link(handle, placetitle, uplink=True), "ColumnPlace"), - (str(etype), "ColumnType") + (str(event.get_type()), "ColumnType") ] ) diff --git a/gramps/plugins/webreport/place.py b/gramps/plugins/webreport/place.py index a95e63632..56850be09 100644 --- a/gramps/plugins/webreport/place.py +++ b/gramps/plugins/webreport/place.py @@ -426,7 +426,7 @@ class PlacePages(BasePage): jsc += MARKERS % ([[plce, latitude, longitude, - 1]], + 1,""]], latitude, longitude, 10) @@ -436,7 +436,7 @@ class PlacePages(BasePage): jsc += MARKER_PATH % marker_path jsc += OSM_MARKERS % ([[float(longitude), float(latitude), - placetitle]], + placetitle,""]], longitude, latitude, 10) jsc += OPENLAYER else: # STAMEN @@ -445,7 +445,7 @@ class PlacePages(BasePage): jsc += MARKER_PATH % marker_path jsc += STAMEN_MARKERS % ([[float(longitude), float(latitude), - placetitle]], + placetitle,""]], self.stamenopts, longitude, latitude, 10) jsc += OPENLAYER