You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-07-03 05:46:58 +02:00
feat(web): Global map showing all assets with geo information (#2355)
* First crude implementation of the global asset map in web * Use single DOM element for all markers * Minor layout changes * Refactor * Add asset viewer * Add API endpoint that returns only assets with location information (Thanks @EPP100) * Remove sidebar icon flip * Add dark theme support * Center map to most recent asset * Allow cluster viewing * Fix linter errors * Add newlines * Fix ts errors * Fix eslint error * Run prettier * Server code style * Fix openapi mobile code generation issues * Map markers test * fix: Support video thumbnails * Update API * Review suggestions * Review suggestions * Linter error * Chage mapMarker endpoint to map-marker * Clean up leaflet imports
This commit is contained in:
@ -0,0 +1,119 @@
|
||||
<script lang="ts" context="module">
|
||||
import { createContext } from '$lib/utils/context';
|
||||
import { MarkerClusterGroup, Marker, Icon, LeafletEvent } from 'leaflet';
|
||||
|
||||
const { get: getContext, set: setClusterContext } = createContext<() => MarkerClusterGroup>();
|
||||
|
||||
export const getClusterContext = () => {
|
||||
return getContext()();
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import 'leaflet.markercluster';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { getMapContext } from './map.svelte';
|
||||
import { MapMarkerResponseDto, api } from '@api';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
class AssetMarker extends Marker {
|
||||
marker: MapMarkerResponseDto;
|
||||
|
||||
constructor(marker: MapMarkerResponseDto) {
|
||||
super([marker.lat, marker.lon], {
|
||||
icon: new Icon({
|
||||
iconUrl: api.getAssetThumbnailUrl(marker.id),
|
||||
iconRetinaUrl: api.getAssetThumbnailUrl(marker.id),
|
||||
iconSize: [60, 60],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
tooltipAnchor: [16, -28],
|
||||
shadowSize: [41, 41]
|
||||
})
|
||||
});
|
||||
|
||||
this.marker = marker;
|
||||
}
|
||||
|
||||
getAssetId(): string {
|
||||
return this.marker.id;
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ view: { assets: string[] } }>();
|
||||
|
||||
export let markers: MapMarkerResponseDto[];
|
||||
|
||||
const map = getMapContext();
|
||||
|
||||
let cluster: MarkerClusterGroup;
|
||||
|
||||
setClusterContext(() => cluster);
|
||||
|
||||
onMount(() => {
|
||||
cluster = new MarkerClusterGroup({
|
||||
showCoverageOnHover: false,
|
||||
zoomToBoundsOnClick: false,
|
||||
spiderfyOnMaxZoom: false,
|
||||
maxClusterRadius: 30,
|
||||
spiderLegPolylineOptions: { opacity: 0 },
|
||||
spiderfyDistanceMultiplier: 3
|
||||
});
|
||||
|
||||
cluster.on('clusterclick', (event: LeafletEvent) => {
|
||||
const ids = event.sourceTarget
|
||||
.getAllChildMarkers()
|
||||
.map((marker: AssetMarker) => marker.getAssetId());
|
||||
dispatch('view', { assets: ids });
|
||||
});
|
||||
|
||||
for (let marker of markers) {
|
||||
const leafletMarker = new AssetMarker(marker);
|
||||
|
||||
leafletMarker.on('click', () => {
|
||||
dispatch('view', { assets: [marker.id] });
|
||||
});
|
||||
|
||||
cluster.addLayer(leafletMarker);
|
||||
}
|
||||
|
||||
map.addLayer(cluster);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (cluster) cluster.remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if cluster}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(.leaflet-marker-icon) {
|
||||
border-radius: 25%;
|
||||
}
|
||||
|
||||
:global(.marker-cluster) {
|
||||
background-clip: padding-box;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
:global(.marker-cluster div) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
|
||||
background-color: rgb(236, 237, 246);
|
||||
color: rgb(69, 80, 169);
|
||||
}
|
||||
|
||||
:global(.marker-cluster span) {
|
||||
line-height: 40px;
|
||||
}
|
||||
</style>
|
@ -1,3 +1,4 @@
|
||||
export { default as Map } from './map.svelte';
|
||||
export { default as Marker } from './marker.svelte';
|
||||
export { default as TileLayer } from './tile-layer.svelte';
|
||||
export { default as AssetMarkerCluster } from './asset-marker-cluster.svelte';
|
||||
|
@ -5,15 +5,30 @@
|
||||
|
||||
export let urlTemplate: string;
|
||||
export let options: TileLayerOptions | undefined = undefined;
|
||||
export let allowDarkMode = false;
|
||||
|
||||
let tileLayer: TileLayer;
|
||||
|
||||
const map = getMapContext();
|
||||
|
||||
onMount(() => {
|
||||
tileLayer = new TileLayer(urlTemplate, options).addTo(map);
|
||||
tileLayer = new TileLayer(urlTemplate, {
|
||||
className: allowDarkMode ? 'leaflet-layer-dynamic' : 'leaflet-layer',
|
||||
...options
|
||||
}).addTo(map);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (tileLayer) tileLayer.remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:global(.leaflet-layer-dynamic) {
|
||||
filter: brightness(100%) contrast(100%) saturate(80%);
|
||||
}
|
||||
|
||||
:global(.dark .leaflet-layer-dynamic) {
|
||||
filter: invert(100%) brightness(130%) saturate(0%);
|
||||
}
|
||||
</style>
|
||||
|
Reference in New Issue
Block a user