Places: Refactor rendering of photos and clusters on the map #1187 #3657

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-09-19 08:41:37 +02:00
parent 9fa7563f0c
commit b93baa8ed0

View file

@ -55,6 +55,7 @@ export default {
canSearch: this.$config.allow("places", "search"),
initialized: false,
map: null,
markers: {},
markersOnScreen: {},
clusterIds: [],
loading: false,
@ -520,77 +521,59 @@ export default {
}
},
getClusterSizeFromItemCount(itemCount) {
if (itemCount >= 750) {
if (itemCount >= 10000) {
return 74;
} else if (itemCount >= 200) {
} else if (itemCount >= 1000) {
return 70;
} else if (itemCount >= 100) {
} else if (itemCount >= 750) {
return 68;
} else if (itemCount >= 200) {
return 66;
} else if (itemCount >= 100) {
return 64;
}
return 60;
},
async updateMarkers() {
if (this.loading) {
return;
abbreviateCount(val) {
const value = Number.parseInt(val);
if (value >= 1000) {
return (value / 1000).toFixed(0).toString() + 'k';
}
// Parts of marker processing are done asyncronously. Ensure previous processing is complete before restarting
if(this.markerPromise) {
await this.markerPromise;
}
this.markerPromise = new Promise((resolve, reject) => {
// Find clusters.
const features = this.map.querySourceFeatures("photos");
const clusterIds = [...new Set(features
.filter(feature => feature.properties.cluster)
.map(feature => feature.properties.cluster_id))];
// Skip update if nothing has changed.
if (clusterIds.toString() === this.clusterIds.toString()) {
resolve("skip");
return;
} else {
this.clusterIds = clusterIds;
}
// If clusterIds are empty, getMultipleClusterFeatures will not call callback,
// and thus resolve will never be called. Handle that case here.
if (clusterIds.length === 0) {
for (let id in this.markersOnScreen) {
this.markersOnScreen[id].remove();
}
this.markersOnScreen = {};
resolve("done");
return;
}
return value;
},
updateMarkers() {
if (this.loading) return;
let newMarkers = {};
this.getMultipleClusterFeatures(clusterIds, (clusterFeaturesById) => {
let features = this.map.querySourceFeatures("photos");
let token = this.$config.previewToken;
for (let i = 0; i < features.length; i++) {
let id = features[i].id;
// Multiple features can exist with the same cluser ID. Avoid processing the same cluster twice
if (!newMarkers[id]) {
let coords = features[i].geometry.coordinates;
let props = features[i].properties;
let token = this.$config.previewToken;
let el = document.createElement('div');
if (props.cluster) {
// Clusters.
let id = -1*props.cluster_id;
let marker = this.markers[id];
if (!marker) {
const size = this.getClusterSizeFromItemCount(props.point_count);
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 clusterFeatures = clusterFeaturesById[props.cluster_id];
const previewImageCount = clusterFeatures.length >= 10 ? 4 : clusterFeatures.length > 1 ? 2 : 1;
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) => {
@ -601,17 +584,38 @@ export default {
});
imageContainer.append(...images);
});
const counterBubble = document.createElement('div');
counterBubble.className = 'counter-bubble primary-button theme--light';
counterBubble.innerText = clusterFeatures.length > 99 ? '99+' : clusterFeatures.length;
counterBubble.innerText = this.abbreviateCount(props.point_count);
el.append(imageContainer);
el.append(counterBubble);
el.addEventListener('click', () => {
this.selectClusterById(props.cluster_id);
});
marker = this.markers[id] = new maplibregl.Marker({
element: el
}).setLngLat(coords);
} else {
marker.setLngLat(coords);
}
newMarkers[id] = marker;
if (!this.markersOnScreen[id]) {
marker.addTo(this.map);
}
} else {
// Pictures.
let id = features[i].id;
let marker = this.markers[id];
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)`;
@ -619,28 +623,28 @@ export default {
el.style.height = '50px';
el.addEventListener('click', () => this.openPhoto(props.UID));
}
let marker = new maplibregl.Marker({
marker = this.markers[id] = new maplibregl.Marker({
element: el
}).setLngLat(coords);
} else {
marker.setLngLat(coords);
}
newMarkers[id] = marker;
if (this.markersOnScreen[id]) {
this.markersOnScreen[id].remove();
}
if (!this.markersOnScreen[id]) {
marker.addTo(this.map);
}
}
}
for (let id in this.markersOnScreen) {
if (!newMarkers[id]) {
this.markersOnScreen[id].remove();
}
}
this.markersOnScreen = newMarkers;
resolve("done");
})
});
},
onMapLoad() {
// Add 'photos' data source.
@ -669,9 +673,9 @@ export default {
});
// 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);
// Load pictures.
this.search();