From d48b77be9edb8db5973837bea98fb99b2327740d Mon Sep 17 00:00:00 2001 From: xaralis <filip.varecha@fragaria.cz> Date: Fri, 27 May 2022 09:34:01 +0100 Subject: [PATCH] feat(maps_utils): improve UX and allow muting individual features --- .../district_geo_feature_detail_page.html | 1 + .../maps_utils/geo-feature-collection.js | 109 ++++++++++++++---- 2 files changed, 89 insertions(+), 21 deletions(-) diff --git a/district/templates/district/district_geo_feature_detail_page.html b/district/templates/district/district_geo_feature_detail_page.html index 00c48618..6d654537 100644 --- a/district/templates/district/district_geo_feature_detail_page.html +++ b/district/templates/district/district_geo_feature_detail_page.html @@ -74,6 +74,7 @@ data-display-zoom-control="false" data-display-legend="false" data-display-popups="false" + data-handle-clicks="false" data-initial-zoom="{{ page.initial_zoom }}" data-tile-server-config="{{ js_map.tile_server_config }}" data-tile-style="{{ js_map.style }}" diff --git a/maps_utils/static/maps_utils/geo-feature-collection.js b/maps_utils/static/maps_utils/geo-feature-collection.js index 9588f2ad..c157372b 100644 --- a/maps_utils/static/maps_utils/geo-feature-collection.js +++ b/maps_utils/static/maps_utils/geo-feature-collection.js @@ -126,6 +126,10 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { type: Boolean, default: true, }, + handleClicks: { + type: Boolean, + default: true, + }, height: { type: String, default: "50rem", @@ -204,9 +208,16 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { }, methods: { initialize(categories, geojson) { - // Annotate with proper id, slug and composed url. + // Annotate with proper composed url and a stash of markers. const annotatedFeatures = geojson.features.map((f) => { + // Identifier in URL. f.properties.url = `${f.id}-${f.slug}`; + // Muted property to control opacity. + f.properties.muted = false; + // Marker stash. + f.properties.markers = []; + // Layer link. + f.properties.layer = null; return f; }); @@ -280,10 +291,10 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { // Get style for given feature. const style = (feature) => ({ fillColor: colorForFeature(feature), - weight: 2, - opacity: 0.7, + weight: 3, + opacity: 0.8, color: colorForFeature(feature), - fillOpacity: 0.5, + fillOpacity: 0.6, }); const markerIconForCategory = (categoryName, number) => @@ -314,6 +325,9 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { offset: [0, -64], }); + // Add marker to feature marker list. + feature.properties.markers.push(featureMarker); + // Add item marker to the cluster. markers.addLayer(featureMarker); @@ -326,7 +340,9 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { */ const pointToLayer = (feature, latlng) => { const onClick = (evt) => { - this.zoomToPoint(evt.latlng, feature); + if (this.handleClicks) { + this.zoomToPoint(evt.latlng, feature); + } }; return addMarker(feature, latlng, onClick); }; @@ -337,7 +353,11 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { */ const onEachFeature = (feature, layer) => { const markerPosLatLng = []; - const onClick = (evt) => this.zoomToLayer(layer, true); + const onClick = (evt) => { + if (this.handleClicks) { + this.zoomToLayer(layer, true); + } + } const markerForPolyCoords = (coords) => { // Find pole of inaccessibility (not centroid) for the polygon @@ -371,14 +391,18 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { **/ if (feature.geometry.type == "Polygon") { markerForPolyCoords(feature.geometry.coordinates); + feature.properties.layer = layer; } else if (feature.geometry.type == "MultiPolygon") { feature.geometry.coordinates.forEach(markerForPolyCoords); + feature.properties.layer = layer; } else if (feature.geometry.type == "LineString") { markerForLineStringCoords(feature.geometry.coordinates); + feature.properties.layer = layer; } else if (feature.geometry.type == "MultiLineString") { feature.geometry.coordinates.forEach( markerForLineStringCoords ); + feature.properties.layer = layer; } else if (feature.geometry.type == "MultiPoint") { // Supported via `pointToLayer`, noop here. } else { @@ -390,10 +414,13 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { if (markerPosLatLng.length) { markerPosLatLng.forEach((pos) => { addMarker(feature, pos, onClick); - // Bind click event on the layer. - layer.on({ - click: (evt) => this.zoomToLayer(evt.target, true), - }); + + if (this.handleClicks) { + // Bind click event on the layer. + layer.on({ + click: (evt) => this.zoomToLayer(evt.target, true), + }); + } }); } }; @@ -453,7 +480,7 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { // If query is present when starting, locate the item and zoom to it. if (urlParams.has("item")) { - this.zoomToFeature(urlParams.get("item"), false); + this.zoomToFeature(urlParams.get("item"), false, true); } else if (this.currentItem) { this.closeItemInfo(false); } @@ -478,15 +505,43 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { toggleExpandCategory(category) { category.expanded = !category.expanded; }, + /** + * Toggle muted state for a feature. + * When feature is muted, it will be less opaque in the list. Used for + * better orientation when there are lots of features. + * @param {Object} feature + */ + toggleMuted(feature) { + feature.properties.muted = !feature.properties.muted; + + feature.properties.markers.forEach((marker) => { + marker.setOpacity(feature.properties.muted ? 0.2 : 1); + }); + + if (feature.properties.layer) { + feature.properties.layer.setStyle(feature.properties.muted ? { + opacity: 0.3, + fillOpacity: 0.2, + } : { + opacity: 0.8, + fillOpacity: 0.6, + }); + } + }, /** * Zoom to a point. * * @param {L.Latlng} Latlng * @param {Object} feature * @param {Boolean} updateUrl whether to push new state to history. + * @param {Boolean} zoom whether to zoom to the item */ - zoomToPoint(latlng, feature, updateUrl = true) { - this.map.panTo(latlng); + zoomToPoint(latlng, feature, updateUrl = true, zoom = false) { + if (zoom) { + this.map.flyTo(latlng, 18); + } else { + this.map.panTo(latlng); + } this.currentItem = feature.properties; if (updateUrl) { @@ -498,9 +553,15 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { * * @param {L.Layer} layer * @param {Boolean} updateUrl whether to push new state to history. + * @param {Boolean} zoom whether to zoom to the items */ - zoomToLayer(layer, updateUrl = true) { - this.map.fitBounds(layer.getBounds()); + zoomToLayer(layer, updateUrl = true, zoom = false) { + if (zoom) { + this.map.flyToBounds(layer.getBounds()); + } else { + this.map.panInsideBounds(layer.getBounds()); + } + this.currentItem = layer.feature.properties; if (updateUrl) { @@ -513,13 +574,18 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { * * @param {String} slugUrl * @param {Boolean} updateUrl whether to push new state to history. + * @param {Boolean} zoom whether to zoom to the items */ - zoomToFeature(slugUrl, updateUrl = true) { + zoomToFeature(slugUrl, updateUrl = true, zoom = false) { const layer = Object.values(this.layer._layers).find( (l) => l.feature.properties.slug == slugUrl ); if (layer) { - this.zoomToLayer(layer, updateUrl); + if (layer.feature.geometry.type == "Point") { + this.zoomToPoint(layer._latlng, layer.feature, updateUrl, zoom); + } else { + this.zoomToLayer(layer, updateUrl, zoom); + } } }, /** @@ -552,10 +618,10 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { <button class="head-allcaps-4xs cursor-pointer" @click="toggleExpandCategory(category)">{{ category.name }} ({{ category.items.length }})</button> </div> <ul v-show="category.expanded" :class="{'mb-2': index != categoryCount - 1}"> - <li v-for="item in category.items" :key="item.properties.title"> - <button @click="zoomToFeature(item.properties.slug)" class="text-left leading-tight text-xs" style="max-width: 17em;"> - <span v-if="item.properties.index" class="rounded-full inline-flex items-center justify-center bg-grey-125 font-bold text-center text-2xs w-4 h-4 mr-1">{{ item.properties.index }}</span> - <span>{{ item.properties.title }}</span> + <li v-for="feature in category.items" :key="feature.properties.title"> + <button @click="toggleMuted(feature)" class="text-left leading-tight text-xs" style="max-width: 17em;" :class="{'opacity-50': feature.properties.muted}"> + <span v-if="feature.properties.index" class="rounded-full inline-flex items-center justify-center bg-grey-125 font-bold text-center text-2xs w-4 h-4 mr-1">{{ feature.properties.index }}</span> + <span>{{ feature.properties.title }}</span> </button> </li> </ul> @@ -610,6 +676,7 @@ Array.from(document.getElementsByClassName("v-geo-feature-collection")).forEach( displayZoomControl: el.dataset.displayZoomControl != "false", displayPopups: el.dataset.displayPopups != "false", + handleClicks: el.dataset.handleClicks != "false", height: el.dataset.height, tileStyle: el.dataset.tileStyle, initialZoom: parseInt(el.dataset.initialZoom), -- GitLab