First crude implementation of the global asset map in web

This commit is contained in:
Matthias Rupp 2023-04-28 21:34:52 -11:00
parent 6acfb55dcc
commit 7b550023a8
11 changed files with 208 additions and 1 deletions

15
web/package-lock.json generated
View file

@ -13,6 +13,7 @@
"handlebars": "^4.7.7",
"justified-layout": "^4.1.0",
"leaflet": "^1.9.3",
"leaflet.markercluster": "^1.5.3",
"lodash-es": "^4.17.21",
"luxon": "^3.2.1",
"rxjs": "^7.8.0",
@ -9044,6 +9045,14 @@
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
},
"node_modules/leaflet.markercluster": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
"peerDependencies": {
"leaflet": "^1.3.1"
}
},
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -18045,6 +18054,12 @@
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
},
"leaflet.markercluster": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
"requires": {}
},
"leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",

View file

@ -61,6 +61,7 @@
"handlebars": "^4.7.7",
"justified-layout": "^4.1.0",
"leaflet": "^1.9.3",
"leaflet.markercluster": "^1.5.3",
"lodash-es": "^4.17.21",
"luxon": "^3.2.1",
"rxjs": "^7.8.0",

View file

@ -93,3 +93,32 @@ input:focus-visible {
scrollbar-width: none;
}
}
/* Leaflet */
.leaflet-marker-icon {
border-radius: 25%;
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.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);
}
.marker-cluster span {
line-height: 40px;
}

View file

@ -0,0 +1,53 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { Marker, Icon, type LatLngExpression, type Content } from 'leaflet';
import { getClusterContext } from './marker-cluster.svelte';
import { AssetResponseDto } from '@api';
export let asset: AssetResponseDto;
let marker: Marker;
const defaultIcon = new Icon({
iconUrl: `/api/asset/thumbnail/${asset.id}?format=WEBP`,
iconRetinaUrl: `/api/asset/thumbnail/${asset.id}?format=WEBP`,
// Default values from Leaflet
iconSize: [40, 40],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowSize: [41, 41]
});
const cluster = getClusterContext();
onMount(() => {
if (asset.exifInfo) {
const lat = asset.exifInfo.latitude;
const lon = asset.exifInfo.longitude;
if (lat && lon) {
marker = new Marker([lat, lon], {
icon: defaultIcon,
alt: ''
});
cluster.addLayer(marker);
}
}
});
onDestroy(() => {
if (marker) marker.remove();
});
$: if (marker) {
if (asset.exifInfo) {
const lat = asset.exifInfo.latitude;
const lon = asset.exifInfo.longitude;
if (lat && lon) {
marker.setLatLng([lat, lon]);
}
}
}
</script>

View file

@ -1,3 +1,5 @@
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 MarkerCluster } from './marker-cluster.svelte';
export { default as AssetClusterMarker } from './asset-cluster-marker.svelte';

View file

@ -40,3 +40,5 @@
<slot />
{/if}
</div>

View file

@ -0,0 +1,40 @@
<script lang="ts" context="module">
import { createContext } from '$lib/utils/context';
const { get: getContext, set: setClusterContext } = createContext<() => L.MarkerClusterGroup>();
export const getClusterContext = () => {
return getContext()();
};
</script>
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import 'leaflet.markercluster';
import { getMapContext } from './map.svelte';
const map = getMapContext();
let cluster: L.MarkerClusterGroup;
setClusterContext(() => cluster);
onMount(() => {
cluster = new L.MarkerClusterGroup({
showCoverageOnHover: false,
zoomToBoundsOnClick: true,
spiderfyOnMaxZoom: true,
maxClusterRadius: 30,
spiderLegPolylineOptions: { opacity: 0 }
});
map.addLayer(cluster);
});
onDestroy(() => {
if (cluster) cluster.remove();
});
</script>
{#if cluster}
<slot />
{/if}

View file

@ -6,6 +6,7 @@
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
import Magnify from 'svelte-material-icons/Magnify.svelte';
import Map from 'svelte-material-icons/Map.svelte';
import StarOutline from 'svelte-material-icons/StarOutline.svelte';
import { AppRoute } from '../../../constants';
import LoadingSpinner from '../loading-spinner.svelte';
@ -108,6 +109,13 @@
isSelected={$page.route.id === '/(user)/explore'}
/>
</a>
<a data-sveltekit-preload-data="hover" href={AppRoute.MAP} draggable="false">
<SideBarButton
title="Map"
logo={Map}
flippedLogo={true}
isSelected={$page.route.id === '/(user)/map'} />
</a>
<a data-sveltekit-preload-data="hover" href={AppRoute.SHARING} draggable="false">
<SideBarButton
title="Sharing"

View file

@ -14,7 +14,8 @@ export enum AppRoute {
EXPLORE = '/explore',
SHARING = '/sharing',
SEARCH = '/search',
MAP = '/map',
AUTH_LOGIN = '/auth/login',
AUTH_LOGOUT = '/auth/logout',
AUTH_REGISTER = '/auth/register',

View file

@ -0,0 +1,23 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { api, user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
try {
const { data: assets } = await api.assetApi.getAllAssets();
return {
user,
assets,
meta: {
title: 'Map'
}
};
} catch (e) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
}) satisfies PageServerLoad;

View file

@ -0,0 +1,33 @@
<script lang="ts">
import type { PageData } from '../map/$types';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import { Map, TileLayer, MarkerCluster, AssetClusterMarker } from '$lib/components/shared-components/leaflet';
export let data: PageData;
</script>
<UserPageLayout user={data.user} title={data.meta.title}>
<div slot="buttons">
</div>
<div class="h-full w-full">
<Map latlng={[48, 11]} zoom={6}>
<TileLayer
urlTemplate={'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'}
options={{
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}}
/>
<MarkerCluster>
{#each data.assets as asset}
<AssetClusterMarker asset={asset} />
{/each}
</MarkerCluster>
</Map>
</div>
</UserPageLayout>