Feature: show geocoded images on the map when hovered, show interactive minimap on nearbyImages element

This commit is contained in:
Pieter Vander Vennet 2024-09-12 01:31:00 +02:00
parent d079ba91aa
commit f3fdc95bd0
23 changed files with 404 additions and 182 deletions

View file

@ -132,6 +132,7 @@
<div class="flex h-32 w-max gap-x-2">
{#each $unknownImages as image (image)}
<AttributedImage
{state}
imgClass="h-32 w-max shrink-0"
image={{ url: image }}
previewedImage={state.previewedImage}

View file

@ -7,10 +7,12 @@
import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
import { UIEventSource } from "../../Logic/UIEventSource"
import { MagnifyingGlassPlusIcon } from "@babeard/svelte-heroicons/outline"
import { CloseButton, Modal } from "flowbite-svelte"
import { CloseButton } from "flowbite-svelte"
import ImageOperations from "./ImageOperations.svelte"
import Popup from "../Base/Popup.svelte"
import { onDestroy } from "svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { Feature, Point } from "geojson"
export let image: Partial<ProvidedImage>
let fallbackImage: string = undefined
@ -20,19 +22,43 @@
let imgEl: HTMLImageElement
export let imgClass: string = undefined
export let state: SpecialVisualizationState = undefined
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
export let previewedImage: UIEventSource<ProvidedImage>
export let canZoom = previewedImage !== undefined
let loaded = false
let showBigPreview = new UIEventSource(false)
onDestroy(showBigPreview.addCallbackAndRun(shown=>{
if(!shown){
let showBigPreview = new UIEventSource(false)
onDestroy(showBigPreview.addCallbackAndRun(shown => {
if (!shown) {
previewedImage.set(false)
}
}))
onDestroy(previewedImage.addCallbackAndRun(previewedImage => {
showBigPreview.set(previewedImage?.id === image.id)
}))
function highlight(entered: boolean = true) {
if (!entered) {
state?.geocodedImages.set([])
return
}
if (isNaN(image.lon) || isNaN(image.lat)) {
return
}
const f: Feature<Point> = {
type: "Feature",
properties: {
id: image.id,
rotation: image.rotation
},
geometry: {
type: "Point",
coordinates: [image.lon, image.lat]
}
}
console.log(f)
state?.geocodedImages.set([f])
}
</script>
<Popup shown={showBigPreview} bodyPadding="p-0" dismissable={true}>
@ -48,7 +74,10 @@
</div>
</Popup>
<div class="relative shrink-0">
<div class="relative w-fit">
<div class="relative w-fit"
on:mouseenter={() => highlight()}
on:mouseleave={() => highlight(false)}
>
<img
bind:this={imgEl}
on:load={() => (loaded = true)}
@ -68,7 +97,7 @@
{#if canZoom && loaded}
<div
class="bg-black-transparent absolute right-0 top-0 rounded-bl-full"
on:click={() => previewedImage.set(image)}>
on:click={() => previewedImage.set(image)}>
<MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" />
</div>
{/if}

View file

@ -8,7 +8,6 @@ import ImageProvider, { ProvidedImage } from "../../Logic/ImageProviders/ImagePr
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Changes } from "../../Logic/Osm/Changes"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Feature } from "geojson"
import SvelteUIElement from "../Base/SvelteUIElement"
import AttributedImage from "./AttributedImage.svelte"
@ -30,6 +29,7 @@ export class ImageCarousel extends Toggle {
try {
let image: BaseUIElement = new SvelteUIElement(AttributedImage, {
image: url,
state,
previewedImage: state?.previewedImage,
})

View file

@ -14,8 +14,8 @@
import AttributedImage from "./AttributedImage.svelte"
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
import LoginToggle from "../Base/LoginToggle.svelte"
import ImagePreview from "./ImagePreview.svelte"
import FloatOver from "../Base/FloatOver.svelte"
import { onDestroy } from "svelte"
import { Utils } from "../../Utils"
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
@ -23,6 +23,8 @@
export let feature: Feature
export let layer: LayerConfig
export let highlighted: UIEventSource<string> = undefined
export let linkable = true
let targetValue = Object.values(image.osmTags)[0]
let isLinked = new UIEventSource(Object.values(tags.data).some((v) => targetValue === v))
@ -33,7 +35,7 @@
key: undefined,
provider: AllImageProviders.byName(image.provider),
date: new Date(image.date),
id: Object.values(image.osmTags)[0],
id: Object.values(image.osmTags)[0]
}
async function applyLink(isLinked: boolean) {
@ -44,7 +46,7 @@
if (isLinked) {
const action = new LinkImageAction(currentTags.id, key, url, tags, {
theme: tags.data._orig_theme ?? state.layout.id,
changeType: "link-image",
changeType: "link-image"
})
await state.changes.applyAction(action)
} else {
@ -53,7 +55,7 @@
if (v === url) {
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, {
theme: tags.data._orig_theme ?? state.layout.id,
changeType: "remove-image",
changeType: "remove-image"
})
state.changes.applyAction(action)
}
@ -62,16 +64,30 @@
}
isLinked.addCallback((isLinked) => applyLink(isLinked))
let element: HTMLDivElement
if (highlighted) {
onDestroy(
highlighted.addCallbackD(highlightedUrl => {
if (highlightedUrl === image.pictureUrl) {
Utils.scrollIntoView(element)
}
})
)
}
</script>
<div
class="flex w-fit shrink-0 flex-col overflow-hidden rounded-lg"
class:border-interactive={$isLinked}
class:border-interactive={$isLinked || $highlighted === image.pictureUrl}
style="border-width: 2px"
bind:this={element}
>
<AttributedImage
{state}
image={providedImage}
imgClass="max-h-64 w-auto"
imgClass="max-h-64 w-auto sm:h-32 md:h-64"
previewedImage={state.previewedImage}
attributionFormat="minimal"
>

View file

@ -7,13 +7,23 @@
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch"
import LinkableImage from "./LinkableImage.svelte"
import type { Feature } from "geojson"
import type { Feature, Point } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Loading from "../Base/Loading.svelte"
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
import MapillaryLink from "../BigComponents/MapillaryLink.svelte"
import MaplibreMap from "../Map/MaplibreMap.svelte"
import { Map as MlMap } from "maplibre-gl"
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import ShowDataLayer from "../Map/ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import * as geocoded_image from "../../assets/generated/layers/geocoded_image.json"
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { onDestroy } from "svelte"
import { BBox } from "../../Logic/BBox"
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
@ -42,12 +52,100 @@
[loadedImages]
)
let asFeatures = result.map(p4cs => p4cs.map(p4c => (<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [p4c.coordinates.lng, p4c.coordinates.lat]
},
properties: {
id: p4c.pictureUrl,
rotation: p4c.direction
}
})))
let selected = new UIEventSource<P4CPicture>(undefined)
let selectedAsFeature = selected.mapD(s => {
return [<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [s.coordinates.lng, s.coordinates.lat]
},
properties: {
id: s.pictureUrl,
selected: "yes",
rotation: s.direction
}
}]
})
let someLoading = imageState.state.mapD((stateRecord) =>
Object.values(stateRecord).some((v) => v === "loading")
)
let errors = imageState.state.mapD((stateRecord) =>
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error")
)
let highlighted = new UIEventSource<string>(undefined)
onDestroy(highlighted.addCallbackD(hl => {
const p4c = result.data?.find(i => i.pictureUrl === hl)
selected.set(p4c)
}
))
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let mapProperties = new MapLibreAdaptor(map, {
rasterLayer: state.mapProperties.rasterLayer,
rotation: state.mapProperties.rotation,
pitch: state.mapProperties.pitch,
zoom: new UIEventSource<number>(16),
location: new UIEventSource({ lon, lat }),
})
const geocodedImageLayer = new LayerConfig(<LayerConfigJson>geocoded_image)
new ShowDataLayer(map, {
features: new StaticFeatureSource(asFeatures),
layer: geocodedImageLayer,
zoomToFeatures: true,
onClick: (feature) => {
highlighted.set(feature.properties.id)
}
})
ShowDataLayer.showMultipleLayers(
map,
new StaticFeatureSource([feature]),
state.layout.layers
)
onDestroy(
asFeatures.addCallbackAndRunD(features => {
if(features.length == 0){
return
}
let bbox = BBox.get(features[0])
for (const f of features) {
bbox = bbox.unionWith(BBox.get(f))
}
mapProperties.maxbounds.set(bbox.pad(1.1))
})
)
new ShowDataLayer(map, {
features: new StaticFeatureSource(selectedAsFeature),
layer: geocodedImageLayer,
onClick: (feature) => {
highlighted.set(feature.properties.id)
}
})
</script>
<div class="flex flex-col">
@ -62,12 +160,24 @@
{:else}
<div class="flex w-full space-x-4 overflow-x-auto" style="scroll-snap-type: x proximity">
{#each $result as image (image.pictureUrl)}
<span class="w-fit shrink-0" style="scroll-snap-align: start">
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} />
<span class="w-fit shrink-0" style="scroll-snap-align: start"
on:mouseenter={() => {highlighted.set(image.pictureUrl)}}
on:mouseleave={() =>{ highlighted.set(undefined); selected.set(undefined)}}
>
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} {highlighted} />
</span>
{/each}
</div>
{/if}
<span class="self-end pt-2">
<MapillaryLink
large={false}
mapProperties={{ zoom: new ImmutableStore(16), location: new ImmutableStore({ lon, lat }) }}
/>
</span>
<div class="my-2 flex justify-between">
<div>
{#if $someLoading && $result.length > 0}
@ -80,9 +190,10 @@
/>
{/if}
</div>
<MapillaryLink
large={false}
mapProperties={{ zoom: new ImmutableStore(16), location: new ImmutableStore({ lon, lat }) }}
/>
</div>
<div class="h-48">
<MaplibreMap interactive={false} {map} {mapProperties} />
</div>
</div>

View file

@ -11,8 +11,9 @@
import Camera_plus from "../../assets/svg/Camera_plus.svelte"
import LoginToggle from "../Base/LoginToggle.svelte"
import { ariaLabel } from "../../Utils/ariaLabel"
import { Accordion, AccordionItem } from "flowbite-svelte"
import { Accordion, AccordionItem, Modal } from "flowbite-svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import Popup from "../Base/Popup.svelte"
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
@ -24,15 +25,16 @@
export let layer: LayerConfig
const t = Translations.t.image.nearby
let expanded = false
let enableLogin = state.featureSwitches.featureSwitchEnableLogin
export let shown = new UIEventSource(false)
</script>
{#if enableLogin.data}
<AccordionSingle>
<span slot="header" class="p-2 text-base">
<button on:click={() => {shown.set(!shown.data)}}><Tr t={t.seeNearby}/> </button>
<Popup {shown} bodyPadding="p-4">
<span slot="header">
<Tr t={t.seeNearby} />
</span>
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer} />
</AccordionSingle>
</Popup>
{/if}

View file

@ -121,7 +121,7 @@
<HeartOutlineIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")} />
{:else if icon === "confirm"}
<Confirm class={clss} {color} />
{:else if icon === "direction"}
{:else if icon === "direction" || icon === "direction_gradient"}
<Direction_gradient class={clss} {color} />
{:else if icon === "not_found"}
<Not_found class={twMerge(clss, "no-image-background")} {color} />

View file

@ -159,10 +159,9 @@ class PointRenderingLayer {
})
if (this._onClick) {
const self = this
el.addEventListener("click", function (ev) {
el.addEventListener("click", (ev)=> {
ev.preventDefault()
self._onClick(feature)
this._onClick(feature)
// Workaround to signal the MapLibreAdaptor to ignore this click
ev["consumed"] = true
})

View file

@ -93,6 +93,7 @@ export interface SpecialVisualizationState {
readonly previewedImage: UIEventSource<ProvidedImage>
readonly nearbyImageSearcher: CombinedFetcher
readonly geolocation: GeoLocationHandler
readonly geocodedImages : UIEventSource<Feature[]>
showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer
reportError(message: string): Promise<void>