First crude implementation of the global asset map in web
This commit is contained in:
parent
6acfb55dcc
commit
7b550023a8
11 changed files with 208 additions and 1 deletions
15
web/package-lock.json
generated
15
web/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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';
|
||||
|
|
|
@ -40,3 +40,5 @@
|
|||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
|
|
23
web/src/routes/(user)/map/+page.server.ts
Normal file
23
web/src/routes/(user)/map/+page.server.ts
Normal 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;
|
33
web/src/routes/(user)/map/+page.svelte
Normal file
33
web/src/routes/(user)/map/+page.svelte
Normal 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:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}}
|
||||
/>
|
||||
<MarkerCluster>
|
||||
{#each data.assets as asset}
|
||||
<AssetClusterMarker asset={asset} />
|
||||
{/each}
|
||||
</MarkerCluster>
|
||||
</Map>
|
||||
</div>
|
||||
|
||||
|
||||
</UserPageLayout>
|
||||
|
Loading…
Add table
Reference in a new issue