Places: Reformat frontend/src/page/places.vue
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
37852a640a
commit
a2cf0c9137
1 changed files with 251 additions and 186 deletions
|
@ -1,10 +1,21 @@
|
|||
<template>
|
||||
<v-container fluid fill-height :class="$config.aclClasses('places')" class="pa-0 p-page p-page-places">
|
||||
<div style="width: 100%; height: 100%; position: relative;">
|
||||
<v-container
|
||||
fluid
|
||||
fill-height
|
||||
:class="$config.aclClasses('places')"
|
||||
class="pa-0 p-page p-page-places"
|
||||
>
|
||||
<div style="width: 100%; height: 100%; position: relative">
|
||||
<div v-if="canSearch" class="map-control search-control">
|
||||
<div class="maplibregl-ctrl maplibregl-ctrl-group map-control-search">
|
||||
<v-text-field v-model.lazy.trim="filter.q"
|
||||
solo hide-details clearable flat single-line validate-on-blur
|
||||
<v-text-field
|
||||
v-model.lazy.trim="filter.q"
|
||||
solo
|
||||
hide-details
|
||||
clearable
|
||||
flat
|
||||
single-line
|
||||
validate-on-blur
|
||||
class="input-search pa-0 ma-0"
|
||||
:label="$gettext('Search')"
|
||||
prepend-inner-icon="search"
|
||||
|
@ -17,7 +28,7 @@
|
|||
></v-text-field>
|
||||
</div>
|
||||
</div>
|
||||
<div id="map" ref="map" style="width: 100%; height: 100%;"></div>
|
||||
<div id="map" ref="map" style="width: 100%; height: 100%"></div>
|
||||
<div v-if="showCluster" class="cluster-control">
|
||||
<v-card class="cluster-control-container">
|
||||
<p-page-photos
|
||||
|
@ -36,19 +47,18 @@
|
|||
import maplibregl from "maplibre-gl";
|
||||
import Api from "common/api";
|
||||
import Thumb from "model/thumb";
|
||||
import PPagePhotos from 'page/photos.vue';
|
||||
import MapStyleControl from 'component/places/style-control';
|
||||
import PPagePhotos from "page/photos.vue";
|
||||
import MapStyleControl from "component/places/style-control";
|
||||
|
||||
export default {
|
||||
name: 'PPagePlaces',
|
||||
name: "PPagePlaces",
|
||||
components: {
|
||||
PPagePhotos,
|
||||
},
|
||||
props: {
|
||||
staticFilter: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
},
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
@ -64,11 +74,12 @@ export default {
|
|||
style: "",
|
||||
mapStyles: [],
|
||||
terrain: {
|
||||
'topo-v2': 'terrain_rgb',
|
||||
'outdoor-v2': 'terrain-rgb',
|
||||
'414c531c-926d-4164-a057-455a215c0eee': 'terrain_rgb_virtual',
|
||||
"topo-v2": "terrain_rgb",
|
||||
"outdoor-v2": "terrain-rgb",
|
||||
"414c531c-926d-4164-a057-455a215c0eee": "terrain_rgb_virtual",
|
||||
},
|
||||
attribution: '<a href="https://www.maptiler.com/copyright/" target="_blank">© MapTiler</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap contributors</a>',
|
||||
attribution:
|
||||
'<a href="https://www.maptiler.com/copyright/" target="_blank">© MapTiler</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap contributors</a>',
|
||||
maxCount: 500000,
|
||||
options: {},
|
||||
mapFont: ["Open Sans Regular"],
|
||||
|
@ -83,7 +94,7 @@ export default {
|
|||
};
|
||||
},
|
||||
watch: {
|
||||
'$route'() {
|
||||
$route() {
|
||||
this.filter.q = this.query();
|
||||
this.filter.s = this.scope();
|
||||
this.initialized = false;
|
||||
|
@ -111,7 +122,7 @@ export default {
|
|||
|
||||
this.lastFilter = {};
|
||||
this.initialized = false;
|
||||
this.$refs.map.innerHTML = '';
|
||||
this.$refs.map.innerHTML = "";
|
||||
|
||||
this.configureMap(style);
|
||||
this.renderMap();
|
||||
|
@ -130,7 +141,7 @@ export default {
|
|||
|
||||
if (this.$config.has("mapKey")) {
|
||||
// Remove non-alphanumeric characters from key.
|
||||
mapKey = this.$config.get("mapKey").replace(/[^a-z0-9]/gi, '');
|
||||
mapKey = this.$config.get("mapKey").replace(/[^a-z0-9]/gi, "");
|
||||
}
|
||||
|
||||
const settings = this.$config.settings();
|
||||
|
@ -196,7 +207,7 @@ export default {
|
|||
{
|
||||
title: this.$gettext("Topographic"),
|
||||
style: "topo-v2",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -221,22 +232,22 @@ export default {
|
|||
mapOptions = {
|
||||
container: "map",
|
||||
style: {
|
||||
"version": 8,
|
||||
"sources": {
|
||||
"world": {
|
||||
"type": "geojson",
|
||||
"data": `${this.$config.staticUri}/geo/world.json`,
|
||||
"maxzoom": 6
|
||||
}
|
||||
version: 8,
|
||||
sources: {
|
||||
world: {
|
||||
type: "geojson",
|
||||
data: `${this.$config.staticUri}/geo/world.json`,
|
||||
maxzoom: 6,
|
||||
},
|
||||
"glyphs": `${this.$config.staticUri}/font/{fontstack}/{range}.pbf`,
|
||||
"layers": [
|
||||
},
|
||||
glyphs: `${this.$config.staticUri}/font/{fontstack}/{range}.pbf`,
|
||||
layers: [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": "#aadafe"
|
||||
}
|
||||
id: "background",
|
||||
type: "background",
|
||||
paint: {
|
||||
"background-color": "#aadafe",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "land",
|
||||
|
@ -248,63 +259,80 @@ export default {
|
|||
},
|
||||
},
|
||||
{
|
||||
"id": "country-abbrev",
|
||||
"type": "symbol",
|
||||
"source": "world",
|
||||
"maxzoom": 3,
|
||||
"layout": {
|
||||
id: "country-abbrev",
|
||||
type: "symbol",
|
||||
source: "world",
|
||||
maxzoom: 3,
|
||||
layout: {
|
||||
"text-field": "{abbrev}",
|
||||
"text-font": ["Open Sans Semibold"],
|
||||
"text-transform": "uppercase",
|
||||
"text-max-width": 20,
|
||||
"text-size": {
|
||||
"stops": [[3, 10], [4, 11], [5, 12], [6, 16]]
|
||||
stops: [
|
||||
[3, 10],
|
||||
[4, 11],
|
||||
[5, 12],
|
||||
[6, 16],
|
||||
],
|
||||
},
|
||||
"text-letter-spacing": {
|
||||
"stops": [[4, 0], [5, 1], [6, 2]]
|
||||
stops: [
|
||||
[4, 0],
|
||||
[5, 1],
|
||||
[6, 2],
|
||||
],
|
||||
},
|
||||
"text-line-height": {
|
||||
"stops": [[5, 1.2], [6, 2]]
|
||||
}
|
||||
stops: [
|
||||
[5, 1.2],
|
||||
[6, 2],
|
||||
],
|
||||
},
|
||||
"paint": {
|
||||
},
|
||||
paint: {
|
||||
"text-halo-color": "#fff",
|
||||
"text-halo-width": 1
|
||||
"text-halo-width": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "country-border",
|
||||
"type": "line",
|
||||
"source": "world",
|
||||
"paint": {
|
||||
id: "country-border",
|
||||
type: "line",
|
||||
source: "world",
|
||||
paint: {
|
||||
"line-color": "#226688",
|
||||
"line-opacity": 0.25,
|
||||
"line-dasharray": [6, 2, 2, 2],
|
||||
"line-width": 1.2
|
||||
}
|
||||
"line-width": 1.2,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "country-name",
|
||||
"type": "symbol",
|
||||
"minzoom": 3,
|
||||
"source": "world",
|
||||
"layout": {
|
||||
id: "country-name",
|
||||
type: "symbol",
|
||||
minzoom: 3,
|
||||
source: "world",
|
||||
layout: {
|
||||
"text-field": "{name}",
|
||||
"text-font": ["Open Sans Semibold"],
|
||||
"text-max-width": 20,
|
||||
"text-size": {
|
||||
"stops": [[3, 10], [4, 11], [5, 12], [6, 16]]
|
||||
}
|
||||
stops: [
|
||||
[3, 10],
|
||||
[4, 11],
|
||||
[5, 12],
|
||||
[6, 16],
|
||||
],
|
||||
},
|
||||
"paint": {
|
||||
},
|
||||
paint: {
|
||||
"text-halo-color": "#fff",
|
||||
"text-halo-width": 1
|
||||
"text-halo-width": 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
attributionControl: false,
|
||||
customAttribution: '',
|
||||
customAttribution: "",
|
||||
zoom: 0,
|
||||
};
|
||||
}
|
||||
|
@ -313,7 +341,7 @@ export default {
|
|||
this.options = mapOptions;
|
||||
},
|
||||
getClusterFromUrl() {
|
||||
const hasLatLng = this.$route.query.latlng !== undefined && this.$route.query.latlng !== '';
|
||||
const hasLatLng = this.$route.query.latlng !== undefined && this.$route.query.latlng !== "";
|
||||
|
||||
if (!hasLatLng) {
|
||||
return undefined;
|
||||
|
@ -342,7 +370,7 @@ export default {
|
|||
this.openCluster({
|
||||
q: this.filter.q,
|
||||
s: this.filter.s,
|
||||
latlng: [latNorth, lngEast, latSouth, lngWest].join(','),
|
||||
latlng: [latNorth, lngEast, latSouth, lngWest].join(","),
|
||||
});
|
||||
},
|
||||
selectClusterById: function (clusterId) {
|
||||
|
@ -376,14 +404,20 @@ export default {
|
|||
this.showCluster = false;
|
||||
},
|
||||
query: function () {
|
||||
return this.$route.query.q ? this.$route.query.q : '';
|
||||
return this.$route.query.q ? this.$route.query.q : "";
|
||||
},
|
||||
scope: function () {
|
||||
return this.$route.params.s ? this.$route.params.s : '';
|
||||
return this.$route.params.s ? this.$route.params.s : "";
|
||||
},
|
||||
openPhoto(uid) {
|
||||
// Abort if uid is empty or results aren't loaded.
|
||||
if (!uid || this.loading || !this.result || !this.result.features || this.result.features.length === 0) {
|
||||
if (
|
||||
!uid ||
|
||||
this.loading ||
|
||||
!this.result ||
|
||||
!this.result.features ||
|
||||
this.result.features.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -402,7 +436,8 @@ export default {
|
|||
this.loading = true;
|
||||
|
||||
// Perform get request to find nearby photos.
|
||||
return Api.get("geo/view", options).then((r) => {
|
||||
return Api.get("geo/view", options)
|
||||
.then((r) => {
|
||||
if (r && r.data && r.data.length > 0) {
|
||||
// Show photos.
|
||||
this.$viewer.show(Thumb.wrap(r.data), 0);
|
||||
|
@ -410,7 +445,8 @@ export default {
|
|||
// Don't open viewer if nothing was found.
|
||||
this.$notify.warn(this.$gettext("No pictures found"));
|
||||
}
|
||||
}).finally(() => {
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
@ -441,7 +477,11 @@ export default {
|
|||
|
||||
if (this.query() !== this.filter.q) {
|
||||
if (this.filter.s) {
|
||||
this.$router.replace({name: "places_view", params: {s: this.filter.s}, query: {q: this.filter.q}});
|
||||
this.$router.replace({
|
||||
name: "places_view",
|
||||
params: { s: this.filter.s },
|
||||
query: { q: this.filter.q },
|
||||
});
|
||||
} else if (this.filter.q) {
|
||||
this.$router.replace({ name: "places", query: { q: this.filter.q } });
|
||||
} else {
|
||||
|
@ -469,7 +509,8 @@ export default {
|
|||
}
|
||||
|
||||
// Do not query the same data more than once unless search results need to be updated.
|
||||
if (this.initialized && JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) return;
|
||||
if (this.initialized && JSON.stringify(this.lastFilter) === JSON.stringify(this.filter))
|
||||
return;
|
||||
this.loading = true;
|
||||
|
||||
this.closeCluster();
|
||||
|
@ -484,7 +525,8 @@ export default {
|
|||
};
|
||||
|
||||
// Fetch results from server.
|
||||
return Api.get("geo", options).then((response) => {
|
||||
return Api.get("geo", options)
|
||||
.then((response) => {
|
||||
if (!response.data.features || response.data.features.length === 0) {
|
||||
this.loading = false;
|
||||
|
||||
|
@ -503,7 +545,7 @@ export default {
|
|||
padding: 100,
|
||||
duration: this.animate,
|
||||
essential: false,
|
||||
animate: true
|
||||
animate: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -511,7 +553,8 @@ export default {
|
|||
this.loading = false;
|
||||
|
||||
this.updateMarkers();
|
||||
}).catch(() => {
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
@ -519,46 +562,65 @@ export default {
|
|||
this.map = new maplibregl.Map(this.options);
|
||||
this.map.setLanguage(this.$config.values.settings.ui.language.split("-")[0]);
|
||||
|
||||
const controlPos = this.$rtl ? 'top-left' : 'top-right';
|
||||
const controlPos = this.$rtl ? "top-left" : "top-right";
|
||||
|
||||
// Show map navigation control.
|
||||
this.map.addControl(new maplibregl.NavigationControl({
|
||||
this.map.addControl(
|
||||
new maplibregl.NavigationControl({
|
||||
visualizePitch: true,
|
||||
showZoom: true,
|
||||
showCompass: true
|
||||
}), controlPos);
|
||||
showCompass: true,
|
||||
}),
|
||||
controlPos
|
||||
);
|
||||
|
||||
// Show terrain control, if supported.
|
||||
if (this.terrain[this.style]) {
|
||||
this.map.addControl(new maplibregl.TerrainControl({
|
||||
this.map.addControl(
|
||||
new maplibregl.TerrainControl({
|
||||
source: this.terrain[this.style],
|
||||
exaggeration: 1
|
||||
}));
|
||||
exaggeration: 1,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Show fullscreen control.
|
||||
this.map.addControl(new maplibregl.FullscreenControl({container: document.querySelector('body')}), controlPos);
|
||||
this.map.addControl(
|
||||
new maplibregl.FullscreenControl({ container: document.querySelector("body") }),
|
||||
controlPos
|
||||
);
|
||||
|
||||
// Show locate control.
|
||||
this.map.addControl(new maplibregl.GeolocateControl({
|
||||
this.map.addControl(
|
||||
new maplibregl.GeolocateControl({
|
||||
positionOptions: {
|
||||
enableHighAccuracy: true
|
||||
enableHighAccuracy: true,
|
||||
},
|
||||
trackUserLocation: true
|
||||
}), controlPos);
|
||||
trackUserLocation: true,
|
||||
}),
|
||||
controlPos
|
||||
);
|
||||
|
||||
// Map style switcher control.
|
||||
if (this.mapStyles.length > 1) {
|
||||
this.map.addControl(new MapStyleControl(this.mapStyles, this.style, this.setStyle), controlPos);
|
||||
this.map.addControl(
|
||||
new MapStyleControl(this.mapStyles, this.style, this.setStyle),
|
||||
controlPos
|
||||
);
|
||||
}
|
||||
|
||||
// Show map scale control.
|
||||
this.map.addControl(new maplibregl.ScaleControl({}), this.$rtl ? 'bottom-right' : 'bottom-left');
|
||||
this.map.addControl(
|
||||
new maplibregl.ScaleControl({}),
|
||||
this.$rtl ? "bottom-right" : "bottom-left"
|
||||
);
|
||||
|
||||
this.map.on("load", () => this.onMapLoad());
|
||||
},
|
||||
getClusterFeatures(clusterId, limit, callback) {
|
||||
this.map.getSource('photos').getClusterLeaves(clusterId, limit, undefined, (error, clusterFeatures) => {
|
||||
this.map
|
||||
.getSource("photos")
|
||||
.getClusterLeaves(clusterId, limit, undefined, (error, clusterFeatures) => {
|
||||
callback(clusterFeatures);
|
||||
});
|
||||
},
|
||||
|
@ -580,7 +642,7 @@ export default {
|
|||
abbreviateCount(val) {
|
||||
const value = Number.parseInt(val);
|
||||
if (value >= 1000) {
|
||||
return (value / 1000).toFixed(0).toString() + 'k';
|
||||
return (value / 1000).toFixed(0).toString() + "k";
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
@ -616,25 +678,29 @@ export default {
|
|||
|
||||
if (!marker) {
|
||||
const size = this.getClusterSizeFromItemCount(props.point_count);
|
||||
let el = document.createElement('div');
|
||||
let el = document.createElement("div");
|
||||
|
||||
el.style.width = `${size}px`;
|
||||
el.style.height = `${size}px`;
|
||||
|
||||
const imageContainer = document.createElement('div');
|
||||
imageContainer.className = 'marker cluster-marker';
|
||||
const imageContainer = document.createElement("div");
|
||||
imageContainer.className = "marker cluster-marker";
|
||||
|
||||
this.map.getSource('photos').getClusterLeaves(props.cluster_id, 4, 0, (error, clusterFeatures) => {
|
||||
this.map
|
||||
.getSource("photos")
|
||||
.getClusterLeaves(props.cluster_id, 4, 0, (error, clusterFeatures) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previewImageCount = clusterFeatures.length >= 4 ? 4 : clusterFeatures.length > 1 ? 2 : 1;
|
||||
const previewImageCount =
|
||||
clusterFeatures.length >= 4 ? 4 : clusterFeatures.length > 1 ? 2 : 1;
|
||||
const images = Array(previewImageCount)
|
||||
.fill(null)
|
||||
.map((a, i) => {
|
||||
const feature = clusterFeatures[Math.floor(clusterFeatures.length * i / previewImageCount)];
|
||||
const image = document.createElement('div');
|
||||
const feature =
|
||||
clusterFeatures[Math.floor((clusterFeatures.length * i) / previewImageCount)];
|
||||
const image = document.createElement("div");
|
||||
image.style.backgroundImage = `url(${this.$config.contentUri}/t/${feature.properties.Hash}/${token}/tile_${50})`;
|
||||
return image;
|
||||
});
|
||||
|
@ -642,19 +708,19 @@ export default {
|
|||
imageContainer.append(...images);
|
||||
});
|
||||
|
||||
const counterBubble = document.createElement('div');
|
||||
const counterBubble = document.createElement("div");
|
||||
|
||||
counterBubble.className = 'counter-bubble primary-button theme--light';
|
||||
counterBubble.className = "counter-bubble primary-button theme--light";
|
||||
counterBubble.innerText = this.abbreviateCount(props.point_count);
|
||||
|
||||
el.append(imageContainer);
|
||||
el.append(counterBubble);
|
||||
el.addEventListener('click', () => {
|
||||
el.addEventListener("click", () => {
|
||||
this.selectClusterById(props.cluster_id);
|
||||
});
|
||||
|
||||
marker = this.markers[id] = new maplibregl.Marker({
|
||||
element: el
|
||||
element: el,
|
||||
}).setLngLat(coords);
|
||||
} else {
|
||||
marker.setLngLat(coords);
|
||||
|
@ -671,16 +737,16 @@ export default {
|
|||
|
||||
let marker = this.markers[id];
|
||||
if (!marker) {
|
||||
let el = document.createElement('div');
|
||||
el.className = 'marker';
|
||||
let el = document.createElement("div");
|
||||
el.className = "marker";
|
||||
el.title = props.Title;
|
||||
el.style.backgroundImage = `url(${this.$config.contentUri}/t/${props.Hash}/${token}/tile_50)`;
|
||||
el.style.width = '50px';
|
||||
el.style.height = '50px';
|
||||
el.style.width = "50px";
|
||||
el.style.height = "50px";
|
||||
|
||||
el.addEventListener('click', () => this.openPhoto(props.UID));
|
||||
el.addEventListener("click", () => this.openPhoto(props.UID));
|
||||
marker = this.markers[id] = new maplibregl.Marker({
|
||||
element: el
|
||||
element: el,
|
||||
}).setLngLat(coords);
|
||||
} else {
|
||||
marker.setLngLat(coords);
|
||||
|
@ -706,40 +772,40 @@ export default {
|
|||
},
|
||||
onMapLoad() {
|
||||
// Add 'photos' data source.
|
||||
this.map.addSource('photos', {
|
||||
type: 'geojson',
|
||||
this.map.addSource("photos", {
|
||||
type: "geojson",
|
||||
data: null,
|
||||
cluster: true,
|
||||
clusterMaxZoom: 18, // Max zoom to cluster points on
|
||||
clusterRadius: 80 // Radius of each cluster when clustering points (defaults to 50)
|
||||
clusterRadius: 80, // Radius of each cluster when clustering points (defaults to 50)
|
||||
});
|
||||
|
||||
// Add 'clusters' layer.
|
||||
this.map.addLayer({
|
||||
id: 'clusters',
|
||||
type: 'circle',
|
||||
source: 'photos',
|
||||
filter: ['has', 'point_count'],
|
||||
id: "clusters",
|
||||
type: "circle",
|
||||
source: "photos",
|
||||
filter: ["has", "point_count"],
|
||||
paint: {
|
||||
'circle-color': '#FFFFFF',
|
||||
'circle-opacity': 0,
|
||||
'circle-radius': 0,
|
||||
"circle-color": "#FFFFFF",
|
||||
"circle-opacity": 0,
|
||||
"circle-radius": 0,
|
||||
},
|
||||
});
|
||||
|
||||
// Example of dynamic map cluster rendering:
|
||||
// https://maplibre.org/maplibre-gl-js/docs/examples/cluster-html/
|
||||
this.map.on('data', (e) => {
|
||||
if (e.sourceId === 'photos' && e.isSourceLoaded) {
|
||||
this.map.on("data", (e) => {
|
||||
if (e.sourceId === "photos" && e.isSourceLoaded) {
|
||||
this.updateMarkers();
|
||||
}
|
||||
});
|
||||
|
||||
// Add additional event handlers to update the marker previews.
|
||||
this.map.on('move', this.updateMarkers);
|
||||
this.map.on('moveend', this.updateMarkers);
|
||||
this.map.on('resize', this.updateMarkers);
|
||||
this.map.on('idle', this.updateMarkers);
|
||||
this.map.on("move", this.updateMarkers);
|
||||
this.map.on("moveend", this.updateMarkers);
|
||||
this.map.on("resize", this.updateMarkers);
|
||||
this.map.on("idle", this.updateMarkers);
|
||||
|
||||
// Load pictures.
|
||||
this.search();
|
||||
|
@ -747,4 +813,3 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
Loading…
Reference in a new issue