From d7413e822897162df913ee41a387b7c987011bdf Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 5 Dec 2023 18:35:18 +0100 Subject: [PATCH] Feature: add zoomable image when clicked --- package-lock.json | 71 ++++++- package.json | 1 + public/css/index-tailwind-output.css | 109 ++++++++-- src/Logic/ImageProviders/Imgur.ts | 4 + src/Logic/ImageProviders/LicenseInfo.ts | 2 + src/Logic/ImageProviders/Mapillary.ts | 4 +- src/Models/ThemeViewState.ts | 3 + src/UI/Base/FloatOver.svelte | 9 +- src/UI/Image/AttributedImage.svelte | 29 +++ src/UI/Image/AttributedImage.ts | 45 ----- src/UI/Image/ImageAttribution.svelte | 66 +++++++ src/UI/Image/ImageCarousel.ts | 12 +- src/UI/Image/ImageOperations.svelte | 41 ++++ src/UI/Image/ImagePreview.svelte | 29 +++ src/UI/{Popup => Image}/LinkableImage.svelte | 44 ++--- src/UI/{Popup => Image}/NearbyImages.svelte | 0 .../NearbyImagesCollapsed.svelte | 0 src/UI/SpecialVisualization.ts | 3 + src/UI/SpecialVisualizations.ts | 4 +- src/UI/ThemeViewGUI.svelte | 186 ++++++++++-------- 20 files changed, 481 insertions(+), 181 deletions(-) create mode 100644 src/UI/Image/AttributedImage.svelte delete mode 100644 src/UI/Image/AttributedImage.ts create mode 100644 src/UI/Image/ImageAttribution.svelte create mode 100644 src/UI/Image/ImageOperations.svelte create mode 100644 src/UI/Image/ImagePreview.svelte rename src/UI/{Popup => Image}/LinkableImage.svelte (54%) rename src/UI/{Popup => Image}/NearbyImages.svelte (100%) rename src/UI/{Popup => Image}/NearbyImagesCollapsed.svelte (100%) diff --git a/package-lock.json b/package-lock.json index 1edc8da62..54874becc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index d51d0025c..7fbbba370 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index ae394847b..6cbce4609 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -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)); diff --git a/src/Logic/ImageProviders/Imgur.ts b/src/Logic/ImageProviders/Imgur.ts index 904221768..ca4cb9429 100644 --- a/src/Logic/ImageProviders/Imgur.ts +++ b/src/Logic/ImageProviders/Imgur.ts @@ -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 } diff --git a/src/Logic/ImageProviders/LicenseInfo.ts b/src/Logic/ImageProviders/LicenseInfo.ts index a75a1ad98..99998e75d 100644 --- a/src/Logic/ImageProviders/LicenseInfo.ts +++ b/src/Logic/ImageProviders/LicenseInfo.ts @@ -9,4 +9,6 @@ export class LicenseInfo { credit: string = "" description: string = "" informationLocation: URL = undefined + date?: Date + views?: number } diff --git a/src/Logic/ImageProviders/Mapillary.ts b/src/Logic/ImageProviders/Mapillary.ts index ed332c4f8..107424cc2 100644 --- a/src/Logic/ImageProviders/Mapillary.ts +++ b/src/Logic/ImageProviders/Mapillary.ts @@ -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, } diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 1db7f7ac5..9962c6b90 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -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(undefined) readonly addNewPoint: UIEventSource = new UIEventSource(false) @@ -475,6 +477,7 @@ export default class ThemeViewState implements SpecialVisualizationState { () => { this.selectedElement.setData(undefined) this.guistate.closeAll() + this.previewedImage.setData(undefined) this.focusOnMap() } ) diff --git a/src/UI/Base/FloatOver.svelte b/src/UI/Base/FloatOver.svelte index bbb2a504e..4268682c3 100644 --- a/src/UI/Base/FloatOver.svelte +++ b/src/UI/Base/FloatOver.svelte @@ -1,15 +1,18 @@
{ dispatch("close") @@ -33,9 +36,9 @@ diff --git a/src/UI/Image/AttributedImage.svelte b/src/UI/Image/AttributedImage.svelte new file mode 100644 index 000000000..2f7576a63 --- /dev/null +++ b/src/UI/Image/AttributedImage.svelte @@ -0,0 +1,29 @@ + + + +
+ { + if(fallbackImage){ + imgEl.src = fallbackImage + } + }}> + +
+ +
+
diff --git a/src/UI/Image/AttributedImage.ts b/src/UI/Image/AttributedImage.ts deleted file mode 100644 index 6a4894c0b..000000000 --- a/src/UI/Image/AttributedImage.ts +++ /dev/null @@ -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") - } -} diff --git a/src/UI/Image/ImageAttribution.svelte b/src/UI/Image/ImageAttribution.svelte new file mode 100644 index 000000000..b9022f8a2 --- /dev/null +++ b/src/UI/Image/ImageAttribution.svelte @@ -0,0 +1,66 @@ + + + +{#if $license !== undefined} +
+ + {#if icon !== undefined} + + {/if} + + +
+ {#if $license.title} + {#if $license.informationLocation} + {$license.title} + {:else} + $license.title + {/if} + {/if} + + {#if $license.artist} +
+ {$license.artist} +
+ {/if} + +
+ + {#if $license.license !== undefined || $license.licenseShortName !== undefined} +
+ {$license?.license ?? $license?.licenseShortName} +
+ {/if} + + {#if $license.date} +
+ {$license.date.toLocaleDateString()} +
+ {/if} +
+ + {#if $license.views} +
+ + {$license.views} +
+ {/if} + +
+
+ +{/if} diff --git a/src/UI/Image/ImageCarousel.ts b/src/UI/Image/ImageCarousel.ts index 70ba392bb..0b6f43d98 100644 --- a/src/UI/Image/ImageCarousel.ts +++ b/src/UI/Image/ImageCarousel.ts @@ -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, - state: { osmConnection?: OsmConnection; changes?: Changes; layout: LayoutConfig }, + state: { osmConnection?: OsmConnection; changes?: Changes; layout: LayoutConfig, previewedImage?: UIEventSource }, 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) diff --git a/src/UI/Image/ImageOperations.svelte b/src/UI/Image/ImageOperations.svelte new file mode 100644 index 000000000..d19c3f109 --- /dev/null +++ b/src/UI/Image/ImageOperations.svelte @@ -0,0 +1,41 @@ + + +
+
+ +
+
+
+ +
+ + +
+ + +
diff --git a/src/UI/Image/ImagePreview.svelte b/src/UI/Image/ImagePreview.svelte new file mode 100644 index 000000000..25413549c --- /dev/null +++ b/src/UI/Image/ImagePreview.svelte @@ -0,0 +1,29 @@ + + + + diff --git a/src/UI/Popup/LinkableImage.svelte b/src/UI/Image/LinkableImage.svelte similarity index 54% rename from src/UI/Popup/LinkableImage.svelte rename to src/UI/Image/LinkableImage.svelte index a4776dc81..ac559bd46 100644 --- a/src/UI/Popup/LinkableImage.svelte +++ b/src/UI/Image/LinkableImage.svelte @@ -1,21 +1,21 @@
- + {#if linkable}