Skip to content
Snippets Groups Projects
Commit d48b77be authored by xaralis's avatar xaralis
Browse files

feat(maps_utils): improve UX and allow muting individual features

parent 8952fa67
No related branches found
No related tags found
2 merge requests!529Release,!528Random fixes/improvements
...@@ -74,6 +74,7 @@ ...@@ -74,6 +74,7 @@
data-display-zoom-control="false" data-display-zoom-control="false"
data-display-legend="false" data-display-legend="false"
data-display-popups="false" data-display-popups="false"
data-handle-clicks="false"
data-initial-zoom="{{ page.initial_zoom }}" data-initial-zoom="{{ page.initial_zoom }}"
data-tile-server-config="{{ js_map.tile_server_config }}" data-tile-server-config="{{ js_map.tile_server_config }}"
data-tile-style="{{ js_map.style }}" data-tile-style="{{ js_map.style }}"
......
...@@ -126,6 +126,10 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -126,6 +126,10 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
handleClicks: {
type: Boolean,
default: true,
},
height: { height: {
type: String, type: String,
default: "50rem", default: "50rem",
...@@ -204,9 +208,16 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -204,9 +208,16 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
}, },
methods: { methods: {
initialize(categories, geojson) { 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) => { const annotatedFeatures = geojson.features.map((f) => {
// Identifier in URL.
f.properties.url = `${f.id}-${f.slug}`; 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; return f;
}); });
...@@ -280,10 +291,10 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -280,10 +291,10 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
// Get style for given feature. // Get style for given feature.
const style = (feature) => ({ const style = (feature) => ({
fillColor: colorForFeature(feature), fillColor: colorForFeature(feature),
weight: 2, weight: 3,
opacity: 0.7, opacity: 0.8,
color: colorForFeature(feature), color: colorForFeature(feature),
fillOpacity: 0.5, fillOpacity: 0.6,
}); });
const markerIconForCategory = (categoryName, number) => const markerIconForCategory = (categoryName, number) =>
...@@ -314,6 +325,9 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -314,6 +325,9 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
offset: [0, -64], offset: [0, -64],
}); });
// Add marker to feature marker list.
feature.properties.markers.push(featureMarker);
// Add item marker to the cluster. // Add item marker to the cluster.
markers.addLayer(featureMarker); markers.addLayer(featureMarker);
...@@ -326,7 +340,9 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -326,7 +340,9 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
*/ */
const pointToLayer = (feature, latlng) => { const pointToLayer = (feature, latlng) => {
const onClick = (evt) => { const onClick = (evt) => {
if (this.handleClicks) {
this.zoomToPoint(evt.latlng, feature); this.zoomToPoint(evt.latlng, feature);
}
}; };
return addMarker(feature, latlng, onClick); return addMarker(feature, latlng, onClick);
}; };
...@@ -337,7 +353,11 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -337,7 +353,11 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
*/ */
const onEachFeature = (feature, layer) => { const onEachFeature = (feature, layer) => {
const markerPosLatLng = []; const markerPosLatLng = [];
const onClick = (evt) => this.zoomToLayer(layer, true); const onClick = (evt) => {
if (this.handleClicks) {
this.zoomToLayer(layer, true);
}
}
const markerForPolyCoords = (coords) => { const markerForPolyCoords = (coords) => {
// Find pole of inaccessibility (not centroid) for the polygon // Find pole of inaccessibility (not centroid) for the polygon
...@@ -371,14 +391,18 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -371,14 +391,18 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
**/ **/
if (feature.geometry.type == "Polygon") { if (feature.geometry.type == "Polygon") {
markerForPolyCoords(feature.geometry.coordinates); markerForPolyCoords(feature.geometry.coordinates);
feature.properties.layer = layer;
} else if (feature.geometry.type == "MultiPolygon") { } else if (feature.geometry.type == "MultiPolygon") {
feature.geometry.coordinates.forEach(markerForPolyCoords); feature.geometry.coordinates.forEach(markerForPolyCoords);
feature.properties.layer = layer;
} else if (feature.geometry.type == "LineString") { } else if (feature.geometry.type == "LineString") {
markerForLineStringCoords(feature.geometry.coordinates); markerForLineStringCoords(feature.geometry.coordinates);
feature.properties.layer = layer;
} else if (feature.geometry.type == "MultiLineString") { } else if (feature.geometry.type == "MultiLineString") {
feature.geometry.coordinates.forEach( feature.geometry.coordinates.forEach(
markerForLineStringCoords markerForLineStringCoords
); );
feature.properties.layer = layer;
} else if (feature.geometry.type == "MultiPoint") { } else if (feature.geometry.type == "MultiPoint") {
// Supported via `pointToLayer`, noop here. // Supported via `pointToLayer`, noop here.
} else { } else {
...@@ -390,10 +414,13 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -390,10 +414,13 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
if (markerPosLatLng.length) { if (markerPosLatLng.length) {
markerPosLatLng.forEach((pos) => { markerPosLatLng.forEach((pos) => {
addMarker(feature, pos, onClick); addMarker(feature, pos, onClick);
if (this.handleClicks) {
// Bind click event on the layer. // Bind click event on the layer.
layer.on({ layer.on({
click: (evt) => this.zoomToLayer(evt.target, true), click: (evt) => this.zoomToLayer(evt.target, true),
}); });
}
}); });
} }
}; };
...@@ -453,7 +480,7 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -453,7 +480,7 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
// If query is present when starting, locate the item and zoom to it. // If query is present when starting, locate the item and zoom to it.
if (urlParams.has("item")) { if (urlParams.has("item")) {
this.zoomToFeature(urlParams.get("item"), false); this.zoomToFeature(urlParams.get("item"), false, true);
} else if (this.currentItem) { } else if (this.currentItem) {
this.closeItemInfo(false); this.closeItemInfo(false);
} }
...@@ -478,15 +505,43 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -478,15 +505,43 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
toggleExpandCategory(category) { toggleExpandCategory(category) {
category.expanded = !category.expanded; 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. * Zoom to a point.
* *
* @param {L.Latlng} Latlng * @param {L.Latlng} Latlng
* @param {Object} feature * @param {Object} feature
* @param {Boolean} updateUrl whether to push new state to history. * @param {Boolean} updateUrl whether to push new state to history.
* @param {Boolean} zoom whether to zoom to the item
*/ */
zoomToPoint(latlng, feature, updateUrl = true) { zoomToPoint(latlng, feature, updateUrl = true, zoom = false) {
if (zoom) {
this.map.flyTo(latlng, 18);
} else {
this.map.panTo(latlng); this.map.panTo(latlng);
}
this.currentItem = feature.properties; this.currentItem = feature.properties;
if (updateUrl) { if (updateUrl) {
...@@ -498,9 +553,15 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -498,9 +553,15 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
* *
* @param {L.Layer} layer * @param {L.Layer} layer
* @param {Boolean} updateUrl whether to push new state to history. * @param {Boolean} updateUrl whether to push new state to history.
* @param {Boolean} zoom whether to zoom to the items
*/ */
zoomToLayer(layer, updateUrl = true) { zoomToLayer(layer, updateUrl = true, zoom = false) {
this.map.fitBounds(layer.getBounds()); if (zoom) {
this.map.flyToBounds(layer.getBounds());
} else {
this.map.panInsideBounds(layer.getBounds());
}
this.currentItem = layer.feature.properties; this.currentItem = layer.feature.properties;
if (updateUrl) { if (updateUrl) {
...@@ -513,13 +574,18 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", { ...@@ -513,13 +574,18 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
* *
* @param {String} slugUrl * @param {String} slugUrl
* @param {Boolean} updateUrl whether to push new state to history. * @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( const layer = Object.values(this.layer._layers).find(
(l) => l.feature.properties.slug == slugUrl (l) => l.feature.properties.slug == slugUrl
); );
if (layer) { 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", { ...@@ -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> <button class="head-allcaps-4xs cursor-pointer" @click="toggleExpandCategory(category)">{{ category.name }} ({{ category.items.length }})</button>
</div> </div>
<ul v-show="category.expanded" :class="{'mb-2': index != categoryCount - 1}"> <ul v-show="category.expanded" :class="{'mb-2': index != categoryCount - 1}">
<li v-for="item in category.items" :key="item.properties.title"> <li v-for="feature in category.items" :key="feature.properties.title">
<button @click="zoomToFeature(item.properties.slug)" class="text-left leading-tight text-xs" style="max-width: 17em;"> <button @click="toggleMuted(feature)" class="text-left leading-tight text-xs" style="max-width: 17em;" :class="{'opacity-50': feature.properties.muted}">
<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 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>{{ item.properties.title }}</span> <span>{{ feature.properties.title }}</span>
</button> </button>
</li> </li>
</ul> </ul>
...@@ -610,6 +676,7 @@ Array.from(document.getElementsByClassName("v-geo-feature-collection")).forEach( ...@@ -610,6 +676,7 @@ Array.from(document.getElementsByClassName("v-geo-feature-collection")).forEach(
displayZoomControl: displayZoomControl:
el.dataset.displayZoomControl != "false", el.dataset.displayZoomControl != "false",
displayPopups: el.dataset.displayPopups != "false", displayPopups: el.dataset.displayPopups != "false",
handleClicks: el.dataset.handleClicks != "false",
height: el.dataset.height, height: el.dataset.height,
tileStyle: el.dataset.tileStyle, tileStyle: el.dataset.tileStyle,
initialZoom: parseInt(el.dataset.initialZoom), initialZoom: parseInt(el.dataset.initialZoom),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment