flotilla/src/app/components/SpaceMemberMap.svelte

118 lines
3.2 KiB
Svelte
Raw Normal View History

<script lang="ts">
import "leaflet/dist/leaflet.css"
import {mount, onMount, unmount} from "svelte"
import type {Map as LeafletMap, Marker, TileLayer} from "leaflet"
import type {Maybe} from "@welshman/lib"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/modal"
import {activeTheme} from "@app/theme"
import type {MemberPin} from "@app/social"
type Props = {
url: string
pins: MemberPin[]
}
const {url, pins}: Props = $props()
const canvas = $derived($activeTheme === "dark" ? "Dark" : "Light")
const tileUrls = $derived(
["Base", "Reference"].map(
layer =>
`https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_${canvas}_Gray_${layer}/MapServer/tile/{z}/{y}/{x}`,
),
)
const openProfile = (pubkey: string) => pushModal(ProfileDetail, {pubkey, url})
let element: HTMLElement
let leaflet: Maybe<typeof import("leaflet")> = $state()
let map: Maybe<LeafletMap> = $state()
$effect(() => {
if (leaflet) {
const created = leaflet
.map(element, {worldCopyJump: true, zoomControl: false})
.setView([25, 0], 2)
leaflet.control.zoom({position: "bottomright"}).addTo(created)
map = created
return () => created.remove()
}
})
$effect(() => {
const tiles: TileLayer[] = []
if (leaflet && map) {
for (const tileUrl of tileUrls) {
tiles.push(
leaflet
.tileLayer(tileUrl, {
maxZoom: 12,
attribution:
'Esri, HERE, Garmin, <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
})
.addTo(map),
)
}
}
return () => tiles.forEach(tile => tile.remove())
})
$effect(() => {
const markers: Marker[] = []
const avatars: Record<string, unknown>[] = []
if (leaflet && map) {
for (const pin of pins) {
const target = document.createElement("div")
avatars.push(
mount(ProfileCircle, {
target,
props: {
pubkey: pin.pubkey,
url,
size: 10,
class: "bg-surface ring-2 ring-surface shadow-lg",
},
}),
)
markers.push(
leaflet
.marker([pin.latitude, pin.longitude], {
title: pin.display,
// An empty className replaces the white box leaflet gives a div icon.
icon: leaflet.divIcon({html: target, className: "", iconSize: [40, 40]}),
})
.on("click", () => openProfile(pin.pubkey))
.addTo(map),
)
}
if (markers.length > 0) {
map.fitBounds(leaflet.featureGroup(markers).getBounds(), {maxZoom: 8, padding: [40, 40]})
}
}
return () => {
markers.forEach(marker => marker.remove())
avatars.forEach(avatar => unmount(avatar))
}
})
onMount(async () => {
leaflet = await import("leaflet")
})
</script>
<!-- Leaflet's panes sit at z-index 400, which covers a modal without a stacking context here. -->
<div bind:this={element} class="isolate min-h-96 flex-1 overflow-hidden rounded-2xl"></div>