immich/web/src/lib/components/shared-components/map/map.svelte
Daniel Dietzler a147dee4b6
feat: Maplibre (#4294)
* maplibre on web, custom styles from server

Actually use new vector tile server, custom style.json

support multiple style files, light/dark mode

cleanup, use new map everywhere

send file directly instead of loading first

better light/dark mode switching

remove leaflet

fix mapstyles dto, first draft of map settings

delete and add styles

fix delete default styles

fix tests

only allow one light and one dark style url

revert config core changes

fix server config store

fix tests

move axios fetches to repo

fix package-lock

fix tests

* open api

* add assets to docker container

* web: use mapSettings color for style

* style: add unique ids to map styles

* mobile: use style json for vector / raster

* do not use svelte-material-icons

* add click events to markers, simplify asset detail map

* improve map performance by using asset thumbnails for markers instead of original file

* Remove custom attribution

(by request)

* mobile: update map attribution

* style: map dark mode

* style: map light mode

* zoom level for state

* styling

* overflow gradient

* Limit maxZoom to 14

* mobile: listen for mapStyle changes in MapThumbnail

* mobile: update concurrency

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: bo0tzz <git@bo0tzz.me>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-11-09 10:10:56 -06:00

146 lines
4.5 KiB
Svelte

<script lang="ts">
import {
MapLibre,
GeoJSON,
MarkerLayer,
AttributionControl,
ControlButton,
Control,
ControlGroup,
Map,
FullscreenControl,
GeolocateControl,
NavigationControl,
ScaleControl,
Popup,
} from 'svelte-maplibre';
import { mapSettings } from '$lib/stores/preferences.store';
import { MapMarkerResponseDto, api } from '@api';
import type { GeoJSONSource, LngLatLike, StyleSpecification } from 'maplibre-gl';
import type { Feature, Geometry, GeoJsonProperties, Point } from 'geojson';
import Icon from '$lib/components/elements/icon.svelte';
import { mdiCog } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
export let mapMarkers: MapMarkerResponseDto[];
export let showSettingsModal: boolean | undefined = undefined;
export let zoom: number | undefined = undefined;
export let center: LngLatLike | undefined = undefined;
export let simplified = false;
$: style = (async () => {
const { data } = await api.systemConfigApi.getMapStyle({ theme: $mapSettings.allowDarkMode ? 'dark' : 'light' });
return data as StyleSpecification;
})();
const dispatch = createEventDispatcher<{ selected: string[] }>();
function handleAssetClick(assetId: string, map: Map | null) {
if (!map) {
return;
}
dispatch('selected', [assetId]);
}
function handleClusterClick(clusterId: number, map: Map | null) {
if (!map) {
return;
}
const mapSource = map?.getSource('geojson') as GeoJSONSource;
mapSource.getClusterLeaves(clusterId, 10000, 0, (error, leaves) => {
if (error) {
return;
}
if (leaves) {
const ids = leaves.map((leaf) => leaf.properties?.id);
dispatch('selected', ids);
}
});
}
type FeaturePoint = Feature<Point, { id: string }>;
const asFeature = (marker: MapMarkerResponseDto): FeaturePoint => {
return {
type: 'Feature',
geometry: { type: 'Point', coordinates: [marker.lon, marker.lat] },
properties: {
id: marker.id,
},
};
};
const asMarker = (feature: Feature<Geometry, GeoJsonProperties>): MapMarkerResponseDto => {
const featurePoint = feature as FeaturePoint;
return {
lat: featurePoint.geometry.coordinates[0],
lon: featurePoint.geometry.coordinates[1],
id: featurePoint.properties.id,
};
};
</script>
{#await style then style}
<MapLibre {style} class="h-full" {center} {zoom} attributionControl={false} let:map>
<NavigationControl position="top-left" showCompass={!simplified} />
{#if !simplified}
<GeolocateControl position="top-left" fitBoundsOptions={{ maxZoom: 12 }} />
<FullscreenControl position="top-left" />
<ScaleControl />
<AttributionControl compact={false} />
{/if}
{#if showSettingsModal !== undefined}
<Control>
<ControlGroup>
<ControlButton on:click={() => (showSettingsModal = true)}><Icon path={mdiCog} size="100%" /></ControlButton>
</ControlGroup>
</Control>
{/if}
<GeoJSON
data={{
type: 'FeatureCollection',
features: mapMarkers.map((marker) => {
return asFeature(marker);
}),
}}
id="geojson"
cluster={{ maxZoom: 14, radius: 500 }}
>
<MarkerLayer
applyToClusters
asButton
let:feature
on:click={(event) => {
handleClusterClick(event.detail.feature.properties.cluster_id, map);
}}
>
<div
class="rounded-full w-[40px] h-[40px] bg-immich-primary text-immich-gray flex justify-center items-center font-mono font-bold shadow-lg hover:bg-immich-dark-primary transition-all duration-200 hover:text-immich-dark-bg opacity-90"
>
{feature.properties?.point_count}
</div>
</MarkerLayer>
<MarkerLayer
applyToClusters={false}
asButton
let:feature
on:click={(event) => {
$$slots.popup || handleAssetClick(event.detail.feature.properties.id, map);
}}
>
<img
src={api.getAssetThumbnailUrl(feature.properties?.id)}
class="rounded-full w-[60px] h-[60px] border-2 border-immich-primary shadow-lg hover:border-immich-dark-primary transition-all duration-200 hover:scale-150"
alt={`Image with id ${feature.properties?.id}`}
/>
{#if $$slots.popup}
<Popup openOn="click" closeOnClickOutside>
<slot name="popup" marker={asMarker(feature)} />
</Popup>
{/if}
</MarkerLayer>
</GeoJSON>
</MapLibre>
{/await}