1
0
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:
Matthias Rupp
2023-05-06 03:33:30 +02:00
committed by GitHub
parent 15a498fd60
commit 65daf342df
28 changed files with 902 additions and 5 deletions

View File

@ -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>

View File

@ -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';

View File

@ -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>