forked from MapComplete/MapComplete
		
	Feature: show geocoded images on the map when hovered, show interactive minimap on nearbyImages element
This commit is contained in:
		
							parent
							
								
									d079ba91aa
								
							
						
					
					
						commit
						f3fdc95bd0
					
				
					 23 changed files with 404 additions and 182 deletions
				
			
		| 
						 | 
				
			
			@ -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}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
                        })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
  >
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue