Feature: add zoomable image when clicked

This commit is contained in:
Pieter Vander Vennet 2023-12-05 18:35:18 +01:00
parent c65ccdbc24
commit d7413e8228
20 changed files with 481 additions and 181 deletions

71
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "mapcomplete",
"version": "0.35.1",
"version": "0.36.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mapcomplete",
"version": "0.35.1",
"version": "0.36.0",
"license": "GPL-3.0-or-later",
"dependencies": {
"@rgossiaux/svelte-headlessui": "^1.0.2",
@ -47,6 +47,7 @@
"opening_hours": "^3.6.0",
"osm-auth": "^2.2.0",
"osmtogeojson": "^3.0.0-beta.5",
"panzoom": "^9.4.3",
"papaparse": "^5.3.1",
"pic4carto": "^2.1.15",
"prompt-sync": "^4.2.0",
@ -4502,6 +4503,14 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/amator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz",
"integrity": "sha512-V5+aH8pe+Z3u/UG3L3pG3BaFQGXAyXHVQDroRwjPHdh08bcUEchAVsU1MCuJSCaU5o60wTK6KaE6te5memzgYw==",
"dependencies": {
"bezier-easing": "^2.0.3"
}
},
"node_modules/ansi-colors": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
@ -4791,6 +4800,11 @@
"tweetnacl": "^0.14.3"
}
},
"node_modules/bezier-easing": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
"integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -9111,6 +9125,11 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
"node_modules/ngraph.events": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz",
"integrity": "sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ=="
},
"node_modules/node-abi": {
"version": "3.31.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.31.0.tgz",
@ -9496,6 +9515,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/panzoom": {
"version": "9.4.3",
"resolved": "https://registry.npmjs.org/panzoom/-/panzoom-9.4.3.tgz",
"integrity": "sha512-xaxCpElcRbQsUtIdwlrZA90P90+BHip4Vda2BC8MEb4tkI05PmR6cKECdqUCZ85ZvBHjpI9htJrZBxV5Gp/q/w==",
"dependencies": {
"amator": "^1.1.0",
"ngraph.events": "^1.2.2",
"wheel": "^1.0.0"
}
},
"node_modules/papaparse": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.2.tgz",
@ -13191,6 +13220,11 @@
"node": ">=14"
}
},
"node_modules/wheel": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wheel/-/wheel-1.0.0.tgz",
"integrity": "sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA=="
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -16778,6 +16812,14 @@
"uri-js": "^4.2.2"
}
},
"amator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz",
"integrity": "sha512-V5+aH8pe+Z3u/UG3L3pG3BaFQGXAyXHVQDroRwjPHdh08bcUEchAVsU1MCuJSCaU5o60wTK6KaE6te5memzgYw==",
"requires": {
"bezier-easing": "^2.0.3"
}
},
"ansi-colors": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
@ -16990,6 +17032,11 @@
"tweetnacl": "^0.14.3"
}
},
"bezier-easing": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
"integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -20236,6 +20283,11 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
"ngraph.events": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz",
"integrity": "sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ=="
},
"node-abi": {
"version": "3.31.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.31.0.tgz",
@ -20523,6 +20575,16 @@
"p-limit": "^3.0.2"
}
},
"panzoom": {
"version": "9.4.3",
"resolved": "https://registry.npmjs.org/panzoom/-/panzoom-9.4.3.tgz",
"integrity": "sha512-xaxCpElcRbQsUtIdwlrZA90P90+BHip4Vda2BC8MEb4tkI05PmR6cKECdqUCZ85ZvBHjpI9htJrZBxV5Gp/q/w==",
"requires": {
"amator": "^1.1.0",
"ngraph.events": "^1.2.2",
"wheel": "^1.0.0"
}
},
"papaparse": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.2.tgz",
@ -23227,6 +23289,11 @@
"webidl-conversions": "^7.0.0"
}
},
"wheel": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wheel/-/wheel-1.0.0.tgz",
"integrity": "sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA=="
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -133,6 +133,7 @@
"opening_hours": "^3.6.0",
"osm-auth": "^2.2.0",
"osmtogeojson": "^3.0.0-beta.5",
"panzoom": "^9.4.3",
"papaparse": "^5.3.1",
"pic4carto": "^2.1.15",
"prompt-sync": "^4.2.0",

View file

@ -729,6 +729,14 @@ video {
bottom: 0px;
}
.right-4 {
right: 1rem;
}
.top-4 {
top: 1rem;
}
.right-1\/3 {
right: 33.333333%;
}
@ -769,6 +777,10 @@ video {
float: left;
}
.m-8 {
margin: 2rem;
}
.m-4 {
margin: 1rem;
}
@ -781,10 +793,6 @@ video {
margin: 0px;
}
.m-8 {
margin: 2rem;
}
.m-2 {
margin: 0.5rem;
}
@ -797,10 +805,58 @@ video {
margin: 0.125rem;
}
.m-11 {
margin: 2.75rem;
}
.m-20 {
margin: 5rem;
}
.m-9 {
margin: 2.25rem;
}
.m-5 {
margin: 1.25rem;
}
.m-14 {
margin: 3.5rem;
}
.m-52 {
margin: 13rem;
}
.m-36 {
margin: 9rem;
}
.m-72 {
margin: 18rem;
}
.m-6 {
margin: 1.5rem;
}
.m-32 {
margin: 8rem;
}
.m-44 {
margin: 11rem;
}
.m-28 {
margin: 7rem;
}
.m-7 {
margin: 1.75rem;
}
.m-px {
margin: 1px;
}
@ -845,6 +901,10 @@ video {
margin-right: 3rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mt-4 {
margin-top: 1rem;
}
@ -881,10 +941,6 @@ video {
margin-right: 0.25rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.ml-1 {
margin-left: 0.25rem;
}
@ -1369,6 +1425,10 @@ video {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.gap-1 {
gap: 0.25rem;
}
@ -1679,6 +1739,10 @@ video {
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-white\/50 {
background-color: rgb(255 255 255 / 0.5);
}
.bg-red-400 {
--tw-bg-opacity: 1;
background-color: rgb(248 113 113 / var(--tw-bg-opacity));
@ -1796,6 +1860,10 @@ video {
padding-left: 1rem;
}
.pr-1 {
padding-right: 0.25rem;
}
.pl-3 {
padding-left: 0.75rem;
}
@ -1804,10 +1872,6 @@ video {
padding-right: 0px;
}
.pr-1 {
padding-right: 0.25rem;
}
.pb-10 {
padding-bottom: 2.5rem;
}
@ -1981,6 +2045,10 @@ video {
opacity: 0.5;
}
.opacity-100 {
opacity: 1;
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
@ -2082,6 +2150,12 @@ video {
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
}
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, -webkit-transform, -webkit-filter, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
@ -2090,10 +2164,8 @@ video {
transition-duration: 150ms;
}
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
.duration-200 {
transition-duration: 200ms;
}
.ease-in-out {
@ -2746,6 +2818,11 @@ a.link-underline {
max-width: 100%;
}
.hover\:bg-white:hover {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.hover\:bg-indigo-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(199 210 254 / var(--tw-bg-opacity));

View file

@ -94,6 +94,8 @@ export class Imgur extends ImageProvider implements ImageUploader {
const descr: string = response.data.description ?? ""
const data: any = {}
const imgurData = response.data
for (const tag of descr.split("\n")) {
const kv = tag.split(":")
const k = kv[0]
@ -104,6 +106,8 @@ export class Imgur extends ImageProvider implements ImageUploader {
licenseInfo.licenseShortName = data.license
licenseInfo.artist = data.author
licenseInfo.date = new Date(Number(imgurData.datetime) * 1000)
licenseInfo.views = imgurData.views
return licenseInfo
}

View file

@ -9,4 +9,6 @@ export class LicenseInfo {
credit: string = ""
description: string = ""
informationLocation: URL = undefined
date?: Date
views?: number
}

View file

@ -60,8 +60,8 @@ export class Mapillary extends ImageProvider {
} = undefined, zoom: number = 17, pKey?: string) {
const params = {
focus: pKey === undefined ? "map" : "photo",
lat: location.lat,
lng: location.lon,
lat: location?.lat,
lng: location?.lon,
z: location === undefined ? undefined : Math.max((zoom ?? 2) - 1, 1),
pKey,
}

View file

@ -59,6 +59,7 @@ import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import { Imgur } from "../Logic/ImageProviders/Imgur"
import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSource"
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
/**
*
@ -113,6 +114,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
readonly geolocation: GeoLocationHandler
readonly imageUploadManager: ImageUploadManager
readonly previewedImage = new UIEventSource<ProvidedImage>(undefined)
readonly addNewPoint: UIEventSource<boolean> = new UIEventSource<boolean>(false)
@ -475,6 +477,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
() => {
this.selectedElement.setData(undefined)
this.guistate.closeAll()
this.previewedImage.setData(undefined)
this.focusOnMap()
}
)

View file

@ -1,15 +1,18 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { twMerge } from "tailwind-merge"
/**
* The slotted element will be shown on top, with a lower-opacity border
*/
const dispatch = createEventDispatcher<{ close }>()
export let extraClasses = "p-4 md:p-6"
</script>
<div
class="absolute top-0 right-0 h-screen w-screen p-4 md:p-6"
class={twMerge("absolute top-0 right-0 h-screen w-screen", extraClasses)}
style="background-color: #00000088; z-index: 20"
on:click={() => {
dispatch("close")
@ -33,9 +36,9 @@
<style>
.content {
height: calc(100vh - 2rem);
height: 100%;
border-radius: 0.5rem;
overflow-x: auto;
overflow-x: hidden;
box-shadow: 0 0 1rem #00000088;
}
</style>

View file

@ -0,0 +1,29 @@
<script lang="ts">
/**
* Shows an image with attribution
*/
import ImageAttribution from "./ImageAttribution.svelte"
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
export let image: ProvidedImage
let fallbackImage: string = undefined
if (image.provider === Mapillary.singleton) {
fallbackImage = "./assets/svg/blocked.svg"
}
let imgEl: HTMLImageElement
</script>
<div class="relative">
<img bind:this={imgEl} src={image.url} on:error={(event) => {
if(fallbackImage){
imgEl.src = fallbackImage
}
}}>
<div class="absolute bottom-0 left-0">
<ImageAttribution {image}/>
</div>
</div>

View file

@ -1,45 +0,0 @@
import Combine from "../Base/Combine"
import Attribution from "./Attribution"
import Img from "../Base/Img"
import ImageProvider from "../../Logic/ImageProviders/ImageProvider"
import BaseUIElement from "../BaseUIElement"
import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Feature } from "geojson"
import { GeoOperations } from "../../Logic/GeoOperations"
export class AttributedImage extends Combine {
constructor(imageInfo: {
id: string,
url: string;
provider?: ImageProvider;
date?: Date
}, feature?: Feature) {
let img: BaseUIElement
img = new Img(imageInfo.url, false, {
fallbackImage:
imageInfo.provider === Mapillary.singleton ? "./assets/svg/blocked.svg" : undefined,
})
let location: {
lon: number,
lat: number
} = undefined
if (feature) {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
location = { lon, lat }
}
let attr: BaseUIElement = undefined
if (imageInfo.provider !== undefined) {
attr = new Attribution(
UIEventSource.FromPromise(imageInfo.provider?.DownloadAttribution(imageInfo.url)),
imageInfo.provider?.SourceIcon(imageInfo.id, location),
imageInfo.date,
)
}
super([img, attr])
this.SetClass("block relative h-full")
}
}

View file

@ -0,0 +1,66 @@
<script lang="ts">
import { LicenseInfo } from "../../Logic/ImageProviders/LicenseInfo"
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import ToSvelte from "../Base/ToSvelte.svelte"
import { EyeIcon } from "@rgossiaux/svelte-heroicons/solid"
/**
* A small element showing the attribution of a single image
*/
export let image: ProvidedImage
let license: Store<LicenseInfo> = UIEventSource.FromPromise(image.provider?.DownloadAttribution(image.url))
let icon = image.provider?.SourceIcon(image.id)?.SetClass("block h-8 w-8 pr-2")
</script>
{#if $license !== undefined}
<div class="flex bg-black text-white text-sm p-0.5 pl-5 pr-3 rounded-lg no-images">
{#if icon !== undefined}
<ToSvelte construct={icon} />
{/if}
<div class="flex flex-col">
{#if $license.title}
{#if $license.informationLocation}
<a href={$license.informationLocation} target="_blank" rel="noopener nofollower">{$license.title}</a>
{:else}
$license.title
{/if}
{/if}
{#if $license.artist}
<div class="font-bold">
{$license.artist}
</div>
{/if}
<div class="flex justify-between">
{#if $license.license !== undefined || $license.licenseShortName !== undefined}
<div>
{$license?.license ?? $license?.licenseShortName}
</div>
{/if}
{#if $license.date}
<div>
{$license.date.toLocaleDateString()}
</div>
{/if}
</div>
{#if $license.views}
<div class="flex justify-around self-center">
<EyeIcon class="w-4 h-4 pr-1"/>
{$license.views}
</div>
{/if}
</div>
</div>
{/if}

View file

@ -1,21 +1,22 @@
import { SlideShow } from "./SlideShow"
import { Store } from "../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Combine from "../Base/Combine"
import DeleteImage from "./DeleteImage"
import { AttributedImage } from "./AttributedImage"
import BaseUIElement from "../BaseUIElement"
import Toggle from "../Input/Toggle"
import ImageProvider from "../../Logic/ImageProviders/ImageProvider"
import ImageProvider, { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
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"
export class ImageCarousel extends Toggle {
constructor(
images: Store<{ id:string, key: string; url: string; provider: ImageProvider }[]>,
tags: Store<any>,
state: { osmConnection?: OsmConnection; changes?: Changes; layout: LayoutConfig },
state: { osmConnection?: OsmConnection; changes?: Changes; layout: LayoutConfig, previewedImage?: UIEventSource<ProvidedImage> },
feature: Feature
) {
const uiElements = images.map(
@ -23,7 +24,7 @@ export class ImageCarousel extends Toggle {
const uiElements: BaseUIElement[] = []
for (const url of imageURLS) {
try {
let image = new AttributedImage(url, feature)
let image: BaseUIElement = new SvelteUIElement(AttributedImage, {image: url})
if (url.key !== undefined) {
image = new Combine([
@ -36,6 +37,7 @@ export class ImageCarousel extends Toggle {
image
.SetClass("w-full block")
.SetStyle("min-width: 50px; background: grey;")
.onClick(() => state.previewedImage.setData(url))
uiElements.push(image)
} catch (e) {
console.error("Could not generate image element for", url.url, "due to", e)

View file

@ -0,0 +1,41 @@
<script lang="ts">/**
* The 'imageOperations' previews an image and offers some extra tools (e.g. download)
*/
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
import ImageAttribution from "./ImageAttribution.svelte"
import ImagePreview from "./ImagePreview.svelte"
import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Utils } from "../../Utils"
export let image: ProvidedImage
async function download() {
const response = await fetch(image.url)
const blob = await response.blob()
Utils.offerContentsAsDownloadableFile(blob, new URL(image.url).pathname.split("/").at(-1), {
mimetype: "image/jpg",
})
}
</script>
<div class="w-full h-full relative">
<div class="absolute top-0 left-0">
<ImagePreview image={image} />
</div>
<div class="absolute bottom-0 left-0 w-full pointer-events-none flex justify-between items-end">
<div class="pointer-events-auto w-fit opacity-50 hover:opacity-100 transition-colors duration-200">
<ImageAttribution image={image} />
</div>
<button
class="no-image-background flex items-center pointer-events-auto bg-black opacity-50 hover:opacity-100 text-white transition-colors duration-200"
on:click={() => download()}>
<DownloadIcon class="w-6 h-6 px-2 opacity-100" />
Download
</button>
</div>
</div>

View file

@ -0,0 +1,29 @@
<script lang="ts">
import { Store } from "../../Logic/UIEventSource"
/**
* The image preview allows to drag and zoom in to the image
*/
import * as panzoom from "panzoom"
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
export let image : ProvidedImage
let panzoomInstance = undefined
let panzoomEl: HTMLElement
$: {
if (panzoomEl) {
panzoomInstance = panzoom(panzoomEl, { bounds: true,
boundsPadding: 1,
minZoom: 1,
maxZoom: 25
})
} else {
panzoomInstance?.dispose()
}
}
</script>
<img bind:this={panzoomEl} src={image.url} />

View file

@ -3,8 +3,6 @@
import type { OsmTags } from "../../Models/OsmFeature"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch"
import ToSvelte from "../Base/ToSvelte.svelte"
import { AttributedImage } from "../Image/AttributedImage"
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import LinkImageAction from "../../Logic/Osm/Actions/LinkImageAction"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
@ -12,8 +10,10 @@
import { GeoOperations } from "../../Logic/GeoOperations"
import type { Feature } from "geojson"
import Translations from "../i18n/Translations"
import SpecialTranslation from "./TagRendering/SpecialTranslation.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
import AttributedImage from "./AttributedImage.svelte"
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
export let tags: Store<OsmTags>
export let lon: number
@ -28,12 +28,12 @@
const t = Translations.t.image.nearby
const c = [lon, lat]
let attributedImage = new AttributedImage({
const providedImage: ProvidedImage = {
url: image.thumbUrl ?? image.pictureUrl,
provider: AllImageProviders.byName(image.provider),
date: new Date(image.date),
id: Object.values(image.osmTags)[0]
}, feature)
}
let distance = Math.round(
GeoOperations.distanceBetween([image.coordinates.lng, image.coordinates.lat], c)
)
@ -64,7 +64,7 @@
</script>
<div class="flex w-fit shrink-0 flex-col">
<ToSvelte construct={attributedImage.SetClass("h-48 w-fit")} />
<AttributedImage image={providedImage} />
{#if linkable}
<label>
<input bind:checked={isLinked} type="checkbox" />

View file

@ -18,6 +18,7 @@ import { RasterLayerPolygon } from "../Models/RasterLayers"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import { OsmTags } from "../Models/OsmFeature"
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
/**
* The state needed to render a special Visualisation.
@ -84,6 +85,8 @@ export interface SpecialVisualizationState {
readonly availableLayers: Store<RasterLayerPolygon[]>
readonly imageUploadManager: ImageUploadManager
readonly previewedImage: UIEventSource<ProvidedImage>
}
export interface SpecialVisualization {

View file

@ -61,8 +61,6 @@ import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import FediverseValidator from "./InputElement/Validators/FediverseValidator"
import SendEmail from "./Popup/SendEmail.svelte"
import NearbyImages from "./Popup/NearbyImages.svelte"
import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte"
import UploadImage from "./Image/UploadImage.svelte"
import { Imgur } from "../Logic/ImageProviders/Imgur"
import Constants from "../Models/Constants"
@ -82,6 +80,8 @@ import OpenJosm from "./Base/OpenJosm.svelte"
import MarkAsFavourite from "./Popup/MarkAsFavourite.svelte"
import MarkAsFavouriteMini from "./Popup/MarkAsFavouriteMini.svelte"
import NextChangeViz from "./OpeningHours/NextChangeViz.svelte"
import NearbyImages from "./Image/NearbyImages.svelte"
import NearbyImagesCollapsed from "./Image/NearbyImagesCollapsed.svelte"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests

View file

@ -1,70 +1,71 @@
<script lang="ts">
import { Store, UIEventSource } from "../Logic/UIEventSource";
import { Map as MlMap } from "maplibre-gl";
import MaplibreMap from "./Map/MaplibreMap.svelte";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import MapControlButton from "./Base/MapControlButton.svelte";
import ToSvelte from "./Base/ToSvelte.svelte";
import If from "./Base/If.svelte";
import { GeolocationControl } from "./BigComponents/GeolocationControl";
import type { Feature } from "geojson";
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import Filterview from "./BigComponents/Filterview.svelte";
import ThemeViewState from "../Models/ThemeViewState";
import type { MapProperties } from "../Models/MapProperties";
import Geosearch from "./BigComponents/Geosearch.svelte";
import Translations from "./i18n/Translations";
import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import Tr from "./Base/Tr.svelte";
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
import FloatOver from "./Base/FloatOver.svelte";
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
import Constants from "../Models/Constants";
import TabbedGroup from "./Base/TabbedGroup.svelte";
import UserRelatedState from "../Logic/State/UserRelatedState";
import LoginToggle from "./Base/LoginToggle.svelte";
import LoginButton from "./Base/LoginButton.svelte";
import CopyrightPanel from "./BigComponents/CopyrightPanel";
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte";
import ModalRight from "./Base/ModalRight.svelte";
import { Utils } from "../Utils";
import Hotkeys from "./Base/Hotkeys";
import { VariableUiElement } from "./Base/VariableUIElement";
import SvelteUIElement from "./Base/SvelteUIElement";
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
import LevelSelector from "./BigComponents/LevelSelector.svelte";
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
import type { RasterLayerPolygon } from "../Models/RasterLayers";
import { AvailableRasterLayers } from "../Models/RasterLayers";
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
import IfHidden from "./Base/IfHidden.svelte";
import { onDestroy } from "svelte";
import MapillaryLink from "./BigComponents/MapillaryLink.svelte";
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte";
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
import StateIndicator from "./BigComponents/StateIndicator.svelte";
import ShareScreen from "./BigComponents/ShareScreen.svelte";
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte";
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte";
import Cross from "../assets/svg/Cross.svelte";
import Summary from "./BigComponents/Summary.svelte";
import LanguagePicker from "./InputElement/LanguagePicker.svelte";
import Mastodon from "../assets/svg/Mastodon.svelte";
import Bug from "../assets/svg/Bug.svelte";
import Liberapay from "../assets/svg/Liberapay.svelte";
import OpenJosm from "./Base/OpenJosm.svelte";
import Min from "../assets/svg/Min.svelte";
import Plus from "../assets/svg/Plus.svelte";
import Filter from "../assets/svg/Filter.svelte";
import Add from "../assets/svg/Add.svelte";
import Statistics from "../assets/svg/Statistics.svelte";
import Community from "../assets/svg/Community.svelte";
import Download from "../assets/svg/Download.svelte";
import Share from "../assets/svg/Share.svelte";
import Favourites from "./Favourites/Favourites.svelte";
import { Store, UIEventSource } from "../Logic/UIEventSource"
import { Map as MlMap } from "maplibre-gl"
import MaplibreMap from "./Map/MaplibreMap.svelte"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import MapControlButton from "./Base/MapControlButton.svelte"
import ToSvelte from "./Base/ToSvelte.svelte"
import If from "./Base/If.svelte"
import { GeolocationControl } from "./BigComponents/GeolocationControl"
import type { Feature } from "geojson"
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import Filterview from "./BigComponents/Filterview.svelte"
import ThemeViewState from "../Models/ThemeViewState"
import type { MapProperties } from "../Models/MapProperties"
import Geosearch from "./BigComponents/Geosearch.svelte"
import Translations from "./i18n/Translations"
import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import Tr from "./Base/Tr.svelte"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
import FloatOver from "./Base/FloatOver.svelte"
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
import Constants from "../Models/Constants"
import TabbedGroup from "./Base/TabbedGroup.svelte"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LoginToggle from "./Base/LoginToggle.svelte"
import LoginButton from "./Base/LoginButton.svelte"
import CopyrightPanel from "./BigComponents/CopyrightPanel"
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
import ModalRight from "./Base/ModalRight.svelte"
import { Utils } from "../Utils"
import Hotkeys from "./Base/Hotkeys"
import { VariableUiElement } from "./Base/VariableUIElement"
import SvelteUIElement from "./Base/SvelteUIElement"
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
import LevelSelector from "./BigComponents/LevelSelector.svelte"
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
import type { RasterLayerPolygon } from "../Models/RasterLayers"
import { AvailableRasterLayers } from "../Models/RasterLayers"
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
import IfHidden from "./Base/IfHidden.svelte"
import { onDestroy } from "svelte"
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
import StateIndicator from "./BigComponents/StateIndicator.svelte"
import ShareScreen from "./BigComponents/ShareScreen.svelte"
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
import Cross from "../assets/svg/Cross.svelte"
import Summary from "./BigComponents/Summary.svelte"
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
import Mastodon from "../assets/svg/Mastodon.svelte"
import Bug from "../assets/svg/Bug.svelte"
import Liberapay from "../assets/svg/Liberapay.svelte"
import OpenJosm from "./Base/OpenJosm.svelte"
import Min from "../assets/svg/Min.svelte"
import Plus from "../assets/svg/Plus.svelte"
import Filter from "../assets/svg/Filter.svelte"
import Add from "../assets/svg/Add.svelte"
import Statistics from "../assets/svg/Statistics.svelte"
import Community from "../assets/svg/Community.svelte"
import Download from "../assets/svg/Download.svelte"
import Share from "../assets/svg/Share.svelte"
import Favourites from "./Favourites/Favourites.svelte"
import ImageOperations from "./Image/ImageOperations.svelte"
export let state: ThemeViewState
let layout = state.layout
@ -73,18 +74,18 @@
let selectedElement: UIEventSource<Feature> = state.selectedElement
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
let currentZoom = state.mapProperties.zoom;
let showCrosshair = state.userRelatedState.showCrosshair;
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation;
let centerFeatures = state.closestFeatures.features;
let currentZoom = state.mapProperties.zoom
let showCrosshair = state.userRelatedState.showCrosshair
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation
let centerFeatures = state.closestFeatures.features
const selectedElementView = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data;
const layer = selectedLayer.data
if (selectedElement === undefined || layer === undefined) {
return undefined;
return undefined
}
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
@ -131,6 +132,7 @@
rasterLayerName = l.properties.name
}),
)
let previewedImage = state.previewedImage
</script>
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
@ -236,7 +238,8 @@
<div class="pointer-events-auto interactive p-1">
{#each $centerFeatures as feat, i (feat.properties.id)}
<div class="flex">
<b>{i+1}.</b><Summary {state} feature={feat}/>
<b>{i + 1}.</b>
<Summary {state} feature={feat} />
</div>
{/each}
</div>
@ -281,6 +284,19 @@
{/if}
</LoginToggle>
<If condition={state.previewedImage.map(i => i!==undefined)}>
<FloatOver on:close={() => state.previewedImage.setData(undefined)} extraClasses="">
<div
slot="close-button"
class="absolute right-4 top-4 h-8 w-8 cursor-pointer rounded-full hover:bg-white bg-white/50 transition-colors duration-200"
on:click={() => previewedImage.setData(undefined)}
>
<XCircleIcon />
</div>
<ImageOperations image={$previewedImage} />
</FloatOver>
</If>
<If
condition={selectedElementView.map(
(v) =>
@ -499,7 +515,9 @@
</div>
<div class="flex flex-col m-2" slot="content2">
<h3> <Tr t={Translations.t.favouritePoi.title}/></h3>
<h3>
<Tr t={Translations.t.favouritePoi.title} />
</h3>
<Favourites {state} />
</div>
<div class="flex" slot="title3">