Places: Reformat frontend/src/page/places.vue

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2024-02-07 16:58:48 +01:00
parent 37852a640a
commit a2cf0c9137

View file

@ -1,23 +1,34 @@
<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
class="input-search pa-0 ma-0"
:label="$gettext('Search')"
prepend-inner-icon="search"
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
color="secondary-dark"
@click:clear="clearQuery"
@keyup.enter.native="formChange"
<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"
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
color="secondary-dark"
@click:clear="clearQuery"
@keyup.enter.native="formChange"
></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,16 +74,17 @@ 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">&copy; MapTiler</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>',
attribution:
'<a href="https://www.maptiler.com/copyright/" target="_blank">&copy; MapTiler</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>',
maxCount: 500000,
options: {},
mapFont: ["Open Sans Regular"],
result: {},
filter: {q: this.query(), s: this.scope()},
filter: { q: this.query(), s: this.scope() },
lastFilter: {},
cluster: {},
showCluster: false,
@ -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,11 +370,11 @@ 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) {
if(this.showCluster) {
if (this.showCluster) {
this.showCluster = false;
}
@ -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,17 +436,19 @@ export default {
this.loading = true;
// Perform get request to find nearby photos.
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);
} else {
// Don't open viewer if nothing was found.
this.$notify.warn(this.$gettext("No pictures found"));
}
}).finally(() => {
this.loading = false;
});
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);
} else {
// Don't open viewer if nothing was found.
this.$notify.warn(this.$gettext("No pictures found"));
}
})
.finally(() => {
this.loading = false;
});
},
formChange() {
if (this.loading) {
@ -441,11 +477,15 @@ 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}});
this.$router.replace({ name: "places", query: { q: this.filter.q } });
} else {
this.$router.replace({name: "places"});
this.$router.replace({ name: "places" });
}
}
},
@ -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,83 +525,104 @@ export default {
};
// Fetch results from server.
return Api.get("geo", options).then((response) => {
if (!response.data.features || response.data.features.length === 0) {
return Api.get("geo", options)
.then((response) => {
if (!response.data.features || response.data.features.length === 0) {
this.loading = false;
this.$notify.warn(this.$gettext("No pictures found"));
return;
}
this.result = response.data;
this.map.getSource("photos").setData(this.result);
if (this.filter.q || !this.initialized) {
this.map.fitBounds(this.result.bbox, {
maxZoom: 17,
padding: 100,
duration: this.animate,
essential: false,
animate: true,
});
}
this.initialized = true;
this.loading = false;
this.$notify.warn(this.$gettext("No pictures found"));
return;
}
this.result = response.data;
this.map.getSource("photos").setData(this.result);
if (this.filter.q || !this.initialized) {
this.map.fitBounds(this.result.bbox, {
maxZoom: 17,
padding: 100,
duration: this.animate,
essential: false,
animate: true
});
}
this.initialized = true;
this.loading = false;
this.updateMarkers();
}).catch(() => {
this.loading = false;
});
this.updateMarkers();
})
.catch(() => {
this.loading = false;
});
},
renderMap() {
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({
visualizePitch: true,
showZoom: true,
showCompass: true
}), controlPos);
this.map.addControl(
new maplibregl.NavigationControl({
visualizePitch: true,
showZoom: true,
showCompass: true,
}),
controlPos
);
// Show terrain control, if supported.
if (this.terrain[this.style]) {
this.map.addControl(new maplibregl.TerrainControl({
source: this.terrain[this.style],
exaggeration: 1
}));
this.map.addControl(
new maplibregl.TerrainControl({
source: this.terrain[this.style],
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({
positionOptions: {
enableHighAccuracy: true
},
trackUserLocation: true
}), controlPos);
this.map.addControl(
new maplibregl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true,
},
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) => {
callback(clusterFeatures);
});
this.map
.getSource("photos")
.getClusterLeaves(clusterId, limit, undefined, (error, clusterFeatures) => {
callback(clusterFeatures);
});
},
getClusterSizeFromItemCount(itemCount) {
if (itemCount >= 10000) {
@ -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,45 +678,49 @@ 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) => {
if (error) {
return;
}
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 images = Array(previewImageCount)
.fill(null)
.map((a, i) => {
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;
});
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");
image.style.backgroundImage = `url(${this.$config.contentUri}/t/${feature.properties.Hash}/${token}/tile_${50})`;
return image;
});
imageContainer.append(...images);
});
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>