photoprism/frontend/src/pages/places.vue
2021-11-27 10:03:12 +01:00

429 lines
12 KiB
Vue

<template>
<v-container fluid fill-height class="pa-0 p-page p-page-places">
<div id="map" style="width: 100%; height: 100%;">
<div class="p-map-control">
<div class="mapboxgl-ctrl mapboxgl-ctrl-group">
<v-text-field v-model="filter.q"
class="pa-0 ma-0 input-search"
single-line
solo
flat
:label="$gettext('Search')"
prepend-inner-icon="search"
clearable
hide-details
browser-autocomplete="off"
color="secondary-dark"
@click:clear="clearQuery"
@keyup.enter.native="formChange"
></v-text-field>
</div>
</div>
</div>
</v-container>
</template>
<script>
import mapboxgl from "mapbox-gl";
import Api from "common/api";
import Thumb from "model/thumb";
export default {
name: 'PPagePlaces',
data() {
return {
initialized: false,
map: null,
markers: {},
markersOnScreen: {},
loading: false,
url: "",
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>',
options: {},
mapFont: [],
result: {},
filter: {q: this.query()},
lastFilter: {},
config: this.$config.values,
settings: this.$config.values.settings.maps,
};
},
watch: {
'$route'() {
this.filter.q = this.query();
this.lastFilter = {};
this.search();
}
},
mounted() {
this.$scrollbar.hide();
this.configureMap().then(() => this.renderMap());
},
destroyed() {
this.$scrollbar.show();
},
methods: {
configureMap() {
return this.$config.load().finally(() => {
const s = this.$config.values.settings.maps;
const filter = {
q: this.query(),
};
let mapKey = "";
if (this.$config.has("mapKey")) {
mapKey = this.$config.get("mapKey");
}
const settings = this.$config.settings();
if (settings && settings.features.private) {
filter.public = true;
}
if (settings && settings.features.review && (!this.staticFilter || !("quality" in this.staticFilter))) {
filter.quality = 3;
}
let mapFont = ['Roboto', 'sans-serif'];
let mapOptions = {
container: "map",
style: "https://api.maptiler.com/maps/" + s.style + "/style.json?key=" + mapKey,
attributionControl: true,
customAttribution: this.attribution,
zoom: 0,
};
if (!mapKey || s.style === "offline") {
mapFont = ["Open Sans Semibold"];
mapOptions = {
container: "map",
style: {
"version": 8,
"sources": {
"world": {
"type": "geojson",
"data": `${this.$config.staticUri}/geo/world.json`,
"maxzoom": 6
}
},
"glyphs": `${this.$config.staticUri}/font/{fontstack}/{range}.pbf`,
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "#aadafe"
}
},
{
id: "land",
type: "fill",
source: "world",
// "source-layer": "land",
paint: {
"fill-color": "#cbe5ca",
},
},
{
"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]]
},
"text-letter-spacing": {
"stops": [[4, 0], [5, 1], [6, 2]]
},
"text-line-height": {
"stops": [[5, 1.2], [6, 2]]
}
},
"paint": {
"text-halo-color": "#fff",
"text-halo-width": 1
},
},
{
"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
}
},
{
"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]]
}
},
"paint": {
"text-halo-color": "#fff",
"text-halo-width": 1
},
},
],
},
attributionControl: true,
customAttribution: this.attribution,
zoom: 0,
};
}
this.filter = filter;
this.url = 'https://api.maptiler.com/maps/' + s.style + '/{z}/{x}/{y}.png?key=' + mapKey;
this.options = mapOptions;
this.mapFont = mapFont;
});
},
query: function () {
return this.$route.params.q ? this.$route.params.q : "";
},
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) {
return;
}
// Get request parameters.
const options = {
params: {
near: uid,
count: 1000,
},
};
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;
});
},
formChange() {
this.search();
},
clearQuery() {
this.filter.q = "";
this.search();
},
updateQuery() {
this.filter.q = this.filter.q.trim();
if (this.query() !== this.filter.q) {
if (this.filter.q) {
this.$router.replace({name: "place", params: {q: this.filter.q}});
} else {
this.$router.replace({name: "places"});
}
}
},
search() {
if (this.loading) return;
// Don't query the same data more than once
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) return;
this.loading = true;
Object.assign(this.lastFilter, this.filter);
this.updateQuery();
const options = {
params: this.filter,
};
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.settings.animate,
essential: false,
animate: this.settings.animate > 0
});
}
this.initialized = true;
this.updateMarkers();
}).finally(() => {
this.loading = false;
});
},
renderMap() {
this.map = new mapboxgl.Map(this.options);
this.map.setLanguage(this.$config.values.settings.ui.language.split("-")[0]);
this.map.addControl(new mapboxgl.NavigationControl({showCompass: true}, 'top-right'));
this.map.addControl(new mapboxgl.FullscreenControl({container: document.querySelector('body')}));
this.map.addControl(new mapboxgl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true
},
trackUserLocation: true
}));
this.map.on("load", () => this.onMapLoad());
},
updateMarkers() {
if (this.loading) return;
let newMarkers = {};
let features = this.map.querySourceFeatures("photos");
for (let i = 0; i < features.length; i++) {
let coords = features[i].geometry.coordinates;
let props = features[i].properties;
if (props.cluster) continue;
let id = features[i].id;
let marker = this.markers[id];
let token = this.$config.previewToken();
if (!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.addEventListener('click', () => this.openPhoto(props.UID));
marker = this.markers[id] = new mapboxgl.Marker({
element: el
}).setLngLat(coords);
} else {
marker.setLngLat(coords);
}
newMarkers[id] = marker;
if (!this.markersOnScreen[id]) {
marker.addTo(this.map);
}
}
for (let id in this.markersOnScreen) {
if (!newMarkers[id]) {
this.markersOnScreen[id].remove();
}
}
this.markersOnScreen = newMarkers;
},
onMapLoad() {
this.map.addSource('photos', {
type: 'geojson',
data: null,
cluster: true,
clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)
});
this.map.addLayer({
id: 'clusters',
type: 'circle',
source: 'photos',
filter: ['has', 'point_count'],
paint: {
'circle-color': [
'step',
['get', 'point_count'],
'#2DC4B2',
100,
'#3BB3C3',
750,
'#669EC4'
],
'circle-radius': [
'step',
['get', 'point_count'],
20,
100,
30,
750,
40
]
}
});
this.map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'photos',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': this.mapFont,
'text-size': 13
}
});
this.map.on('render', this.updateMarkers);
this.map.on('click', 'clusters', (e) => {
const features = this.map.queryRenderedFeatures(e.point, {
layers: ['clusters']
});
const clusterId = features[0].properties.cluster_id;
this.map.getSource('photos').getClusterExpansionZoom(
clusterId,
(err, zoom) => {
if (err) return;
this.map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom
});
}
);
});
this.map.on('mouseenter', 'clusters', () => {
this.map.getCanvas().style.cursor = 'pointer';
});
this.map.on('mouseleave', 'clusters', () => {
this.map.getCanvas().style.cursor = '';
});
this.search();
},
},
};
</script>