From 44223d0f1ccbc42ab4db178bc662d455a5f1f698 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 3 Jun 2022 01:33:41 +0200 Subject: [PATCH] Add mapillary and other nearby images preview --- Logic/ElementStorage.ts | 1 + Logic/GeoOperations.ts | 7 + Logic/ImageProviders/Mapillary.ts | 3 +- Logic/Osm/OsmConnection.ts | 4 +- Logic/Osm/OsmPreferences.ts | 4 +- UI/Input/Checkboxes.ts | 7 +- UI/Input/Slider.ts | 51 +++++++ UI/Popup/NearbyImages.ts | 242 +++++++++++++++++++++++------- UI/SpecialVisualizations.ts | 58 ++++--- assets/svg/filter_disable.svg | 49 ++++++ css/index-tailwind-output.css | 116 ++++++++------ langs/en.json | 6 +- 12 files changed, 418 insertions(+), 130 deletions(-) create mode 100644 UI/Input/Slider.ts create mode 100644 assets/svg/filter_disable.svg diff --git a/Logic/ElementStorage.ts b/Logic/ElementStorage.ts index 1ab5dc9bc..2a23a9054 100644 --- a/Logic/ElementStorage.ts +++ b/Logic/ElementStorage.ts @@ -2,6 +2,7 @@ * Keeps track of a dictionary 'elementID' -> UIEventSource */ import {UIEventSource} from "./UIEventSource"; +import {GeoJSONObject} from "@turf/turf"; export class ElementStorage { diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index df99a707a..c5b5a5f40 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -3,6 +3,7 @@ import {BBox} from "./BBox"; import togpx from "togpx" import Constants from "../Models/Constants"; import LayerConfig from "../Models/ThemeConfig/LayerConfig"; +import {Coord} from "@turf/turf"; export class GeoOperations { @@ -729,6 +730,12 @@ export class GeoOperations { } + /** + * Takes two points and finds the geographic bearing between them, i.e. the angle measured in degrees from the north line (0 degrees) + */ + public static bearing(a: Coord, b: Coord): number { + return turf.bearing(a, b) + } } diff --git a/Logic/ImageProviders/Mapillary.ts b/Logic/ImageProviders/Mapillary.ts index 9d062f786..1bd655583 100644 --- a/Logic/ImageProviders/Mapillary.ts +++ b/Logic/ImageProviders/Mapillary.ts @@ -26,7 +26,6 @@ export class Mapillary extends ImageProvider { return true } try { -console.log("COmparing",a,b) const aUrl = new URL(a) const bUrl = new URL(b) if (aUrl.host !== bUrl.host || aUrl.pathname !== bUrl.pathname) { @@ -46,7 +45,7 @@ console.log("COmparing",a,b) return allSame; } catch (e) { - Console.debug("Could not compare ", a, "and", b, "due to", e) + console.debug("Could not compare ", a, "and", b, "due to", e) } return false; diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 5b8408583..aaa8f92cf 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -122,8 +122,8 @@ export class OsmConnection { return new ChangesetHandler(this._dryRun, this, allElements, changes, this.auth); } - public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { - return this.preferencesHandler.GetPreference(key, prefix); + public GetPreference(key: string, defaultValue: string = undefined, prefix: string = "mapcomplete-"): UIEventSource { + return this.preferencesHandler.GetPreference(key, defaultValue, prefix); } public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { diff --git a/Logic/Osm/OsmPreferences.ts b/Logic/Osm/OsmPreferences.ts index dc9b5c50e..f9245c18f 100644 --- a/Logic/Osm/OsmPreferences.ts +++ b/Logic/Osm/OsmPreferences.ts @@ -106,7 +106,7 @@ export class OsmPreferences { return source; } - public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { + public GetPreference(key: string, defaultValue : string = undefined, prefix: string = "mapcomplete-"): UIEventSource { key = prefix + key; key = key.replace(/[:\\\/"' {}.%]/g, '') if (key.length >= 255) { @@ -120,7 +120,7 @@ export class OsmPreferences { this.UpdatePreferences(); } - const pref = new UIEventSource(this.preferences.data[key], "osm-preference:" + key); + const pref = new UIEventSource(this.preferences.data[key] ?? defaultValue, "osm-preference:" + key); pref.addCallback((v) => { this.UploadPreference(key, v); }); diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index cf849081e..3b0e1da96 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -19,7 +19,8 @@ export class CheckBox extends InputElementMap { } /** - * Supports multi-input + * A list of individual checkboxes + * The value will contain the indexes of the selected checkboxes */ export default class CheckBoxes extends InputElement { private static _nextId = 0; @@ -86,9 +87,7 @@ export default class CheckBoxes extends InputElement { formTag.appendChild(wrapper); value.addCallbackAndRunD((selectedValues) => { - if (selectedValues.indexOf(i) >= 0) { - input.checked = true; - } + input.checked = selectedValues.indexOf(i) >= 0; if (input.checked) { wrapper.classList.remove("border-gray-400"); diff --git a/UI/Input/Slider.ts b/UI/Input/Slider.ts new file mode 100644 index 000000000..c1411cf0f --- /dev/null +++ b/UI/Input/Slider.ts @@ -0,0 +1,51 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import doc = Mocha.reporters.doc; + +export default class Slider extends InputElement { + + private readonly _value: UIEventSource + private min: number; + private max: number; + private step: number; + + /** + * Constructs a slider input element for natural numbers + * @param min: the minimum value that is allowed, inclusive + * @param max: the max value that is allowed, inclusive + * @param options: value: injectable value; step: the step size of the slider + */ + constructor(min: number, max: number, options?: { + value?: UIEventSource, + step?: 1 | number + }) { + super(); + this.max = max; + this.min = min; + this._value = options?.value ?? new UIEventSource(min) + this.step = options?.step ?? 1; + } + + GetValue(): UIEventSource { + return this._value; + } + + protected InnerConstructElement(): HTMLElement { + const el = document.createElement("input") + el.type = "range" + el.min = "" + this.min + el.max = "" + this.max + el.step = "" + this.step + const valuestore = this._value + el.oninput = () => { + valuestore.setData(Number(el.value)) + } + valuestore.addCallbackAndRunD(v => el.value = ""+valuestore.data) + return el; + } + + IsValid(t: number): boolean { + return Math.round(t) == t && t >= this.min && t <= this.max; + } + +} \ No newline at end of file diff --git a/UI/Popup/NearbyImages.ts b/UI/Popup/NearbyImages.ts index 1143df5e8..772da20c7 100644 --- a/UI/Popup/NearbyImages.ts +++ b/UI/Popup/NearbyImages.ts @@ -12,17 +12,22 @@ import {VariableUiElement} from "../Base/VariableUIElement"; import Translations from "../i18n/Translations"; import {Mapillary} from "../../Logic/ImageProviders/Mapillary"; import {SubtleButton} from "../Base/SubtleButton"; +import {GeoOperations} from "../../Logic/GeoOperations"; +import {ElementStorage} from "../../Logic/ElementStorage"; +import Lazy from "../Base/Lazy"; +import {Utils} from "../../Utils"; +import beginningOfLine = Mocha.reporters.Base.cursor.beginningOfLine; export interface P4CPicture { pictureUrl: string, - date: number, + date?: number, coordinates: { lat: number, lng: number }, provider: "Mapillary" | string, - author, - license, - detailsUrl: string, - direction, - osmTags: object /*To copy straight into OSM!*/ + author?, + license?, + detailsUrl?: string, + direction?, + osmTags?: object /*To copy straight into OSM!*/ , thumbUrl: string, details: { @@ -34,56 +39,189 @@ export interface P4CPicture { export interface NearbyImageOptions { lon: number, lat: number, - radius: number, + // Radius of the upstream search + searchRadius?: 500 | number, maxDaysOld?: 1095 | number, - blacklist: UIEventSource<{url: string}[]>, + blacklist: UIEventSource<{ url: string }[]>, shownImagesCount?: UIEventSource, - towardscenter?: boolean; + towardscenter?: UIEventSource; + allowSpherical?: UIEventSource + // Radius of what is shown. Useless to select a value > searchRadius; defaults to searchRadius + shownRadius?: UIEventSource } -export default class NearbyImages extends VariableUiElement { +class ImagesInLoadedDataFetcher { + private allElements: ElementStorage; - constructor(options: NearbyImageOptions) { - const t = Translations.t.image.nearbyPictures + constructor(state: { allElements: ElementStorage }) { + this.allElements = state.allElements + } + + public fetchAround(loc: { lon: number, lat: number, searchRadius?: number }): P4CPicture[] { + const foundImages: P4CPicture[] = [] + this.allElements.ContainingFeatures.forEach((feature) => { + const props = feature.properties; + const images = [] + if (props.image) { + images.push(props.image) + } + for (let i = 0; i < 10; i++) { + + if (props["image:" + i]) { + images.push(props["image:" + i]) + } + } + if (images.length == 0) { + return; + } + const centerpoint = GeoOperations.centerpointCoordinates(feature) + const d = GeoOperations.distanceBetween(centerpoint, [loc.lon, loc.lat]) + if (loc.searchRadius !== undefined && d > loc.searchRadius) { + return; + } + for (const image of images) { + foundImages.push({ + pictureUrl: image, + thumbUrl: image, + coordinates: {lng: centerpoint[0], lat: centerpoint[1]}, + provider: "OpenStreetMap", + details: { + isSpherical: false + } + }) + } + + }) + const cleaned: P4CPicture[] = [] + const seen = new Set() + for (const foundImage of foundImages) { + if (seen.has(foundImage.pictureUrl)) { + continue + } + seen.add(foundImage.pictureUrl) + cleaned.push(foundImage) + } + return cleaned + } +} + +export default class NearbyImages extends Lazy { + + constructor(options: NearbyImageOptions, state?: { allElements: ElementStorage }) { + super(() => { + + const t = Translations.t.image.nearbyPictures + const shownImages = options.shownImagesCount ?? new UIEventSource(25); + + const loadedPictures = NearbyImages.buildPictureFetcher(options, state) + + const loadMoreButton = new Combine([new SubtleButton(Svg.add_svg(), t.loadMore).onClick(() => { + shownImages.setData(shownImages.data + 25) + })]).SetClass("flex flex-col justify-center") + + const imageElements = loadedPictures.map(imgs => { + if(imgs === undefined){ + return [] + } + const elements = (imgs.images ?? []).slice(0, shownImages.data).map(i => this.prepareElement(i)); + if (imgs.images !== undefined && elements.length < imgs.images.length) { + // We effectively sliced some items, so we can increase the count + elements.push(loadMoreButton) + } + return elements; + }, [shownImages]); + + return new VariableUiElement(loadedPictures.map(loaded => { + + if (loaded?.images === undefined) { + return NearbyImages.NoImagesView(new Loading(t.loading)) + } + const images = loaded.images + const beforeFilter = loaded?.beforeFilter + if (beforeFilter === 0) { + return NearbyImages.NoImagesView(t.nothingFound.SetClass("alert block")) + }else if(images.length === 0){ + + const removeFiltersButton = new SubtleButton(Svg.filter_disable_svg(), t.removeFilters).onClick(() => { + options.shownRadius.setData(options.searchRadius) + options.allowSpherical.setData(true) + options.towardscenter.setData(false) + }); + + return NearbyImages.NoImagesView( + t.allFiltered.SetClass("font-bold"), + removeFiltersButton + ) + } + + return new SlideShow(imageElements) + },)); + + }) + } + + private static NoImagesView(...elems: BaseUIElement[]){ + return new Combine(elems).SetClass("flex flex-col justify-center items-center bg-gray-200 mb-2 rounded-lg") + .SetStyle("height: calc( var(--image-carousel-height) - 0.5rem ) ; max-height: calc( var(--image-carousel-height) - 0.5rem );") + } + + private static buildPictureFetcher(options: NearbyImageOptions, state?: { allElements: ElementStorage }) { const P4C = require("../../vendor/P4C.min") const picManager = new P4C.PicturesManager({}); - const shownImages = options.shownImagesCount ?? new UIEventSource(25); - const loadedPictures = - UIEventSource.FromPromise( - picManager.startPicsRetrievalAround(new P4C.LatLng(options.lat, options.lon), options.radius, { - mindate: new Date().getTime() - (options.maxDaysOld ?? (3*365)) * 24 * 60 * 60 * 1000, - towardscenter: options.towardscenter + const searchRadius = options.searchRadius ?? 500; + + const nearbyImages = state !== undefined ? new ImagesInLoadedDataFetcher(state).fetchAround(options) : [] + + + return UIEventSource.FromPromise( + picManager.startPicsRetrievalAround(new P4C.LatLng(options.lat, options.lon), options.searchRadius ?? 500, { + mindate: new Date().getTime() - (options.maxDaysOld ?? (3 * 365)) * 24 * 60 * 60 * 1000, + towardscenter: false }) ).map(images => { - console.log("Images are" ,images, "blacklisted is", options.blacklist.data) - images?.sort((a, b) => b.date - a.date) - return images ?.filter(i => !(options.blacklist?.data?.some(blacklisted => - Mapillary.sameUrl(i.pictureUrl, blacklisted.url))) - && i.details.isSpherical === false); - }, [options.blacklist]) + if (images === undefined && nearbyImages.length === 0) { + return undefined + } + images = (images ?? []).concat(nearbyImages) + const blacklisted = options.blacklist?.data + images = images?.filter(i => !blacklisted?.some(notAllowed => Mapillary.sameUrl(i.pictureUrl, notAllowed.url))); - const loadMoreButton = new Combine([new SubtleButton(Svg.add_svg(), t.loadMore).onClick(() => { - shownImages.setData(shownImages.data + 25) - })]).SetClass("flex flex-col justify-center") - const imageElements = loadedPictures.map(imgs => { - const elements = (imgs ?? []).slice(0, shownImages.data).map(i => this.prepareElement(i)); - if(imgs !== undefined && elements.length < imgs.length){ - // We effectively sliced some items, so we can increase the count - elements.push(loadMoreButton) + const beforeFilterCount = images.length + + if (!(options?.allowSpherical?.data)) { + images = images?.filter(i => i.details.isSpherical !== true) } - return elements; - },[shownImages]); - - super(loadedPictures.map(images => { - if(images === undefined){ - return new Loading(t.loading); + + const shownRadius = options?.shownRadius?.data ?? searchRadius; + if (shownRadius !== searchRadius) { + images = images.filter(i => { + const d = GeoOperations.distanceBetween([i.coordinates.lng, i.coordinates.lat], [options.lon, options.lat]) + return d <= shownRadius + }) } - if(images.length === 0){ - return t.nothingFound.SetClass("alert block") + if (options.towardscenter?.data) { + images = images.filter(i => { + if (i.direction === undefined || isNaN(i.direction)) { + return false + } + const bearing = GeoOperations.bearing([i.coordinates.lng, i.coordinates.lat], [options.lon, options.lat]) + const diff = Math.abs((i.direction - bearing) % 360); + return diff < 40 + }) } - return new SlideShow(imageElements) - })); - + + images?.sort((a, b) => { + const distanceA = GeoOperations.distanceBetween([a.coordinates.lng, a.coordinates.lat], [options.lon, options.lat]) + const distanceB = GeoOperations.distanceBetween([b.coordinates.lng, b.coordinates.lat], [options.lon, options.lat]) + return distanceA - distanceB + }) + + + return {images, beforeFilter: beforeFilterCount}; + + }, [options.blacklist, options.allowSpherical, options.towardscenter, options.shownRadius]) + + } protected prepareElement(info: P4CPicture): BaseUIElement { @@ -91,14 +229,14 @@ export default class NearbyImages extends VariableUiElement { return new AttributedImage({url: info.pictureUrl, provider}) } - private asAttributedImage(info: P4CPicture): AttributedImage { + private static asAttributedImage(info: P4CPicture): AttributedImage { const provider = AllImageProviders.byName(info.provider); return new AttributedImage({url: info.thumbUrl, provider, date: new Date(info.date)}) } - - protected asToggle(info:P4CPicture): Toggle { - const imgNonSelected = this.asAttributedImage(info); - const imageSelected = this.asAttributedImage(info); + + protected asToggle(info: P4CPicture): Toggle { + const imgNonSelected = NearbyImages.asAttributedImage(info); + const imageSelected = NearbyImages.asAttributedImage(info); const nonSelected = new Combine([imgNonSelected]).SetClass("relative block") const hoveringCheckmark = @@ -117,8 +255,8 @@ export default class NearbyImages extends VariableUiElement { export class SelectOneNearbyImage extends NearbyImages implements InputElement { private readonly value: UIEventSource; - constructor(options: NearbyImageOptions & {value?: UIEventSource}) { - super(options) + constructor(options: NearbyImageOptions & { value?: UIEventSource }, state?: { allElements: ElementStorage }) { + super(options, state) this.value = options.value ?? new UIEventSource(undefined); } @@ -135,11 +273,13 @@ export class SelectOneNearbyImage extends NearbyImages implements InputElement

{ if (enabled) { this.value.setData(info) + } else if (this.value.data === info) { + this.value.setData(undefined) } }) this.value.addCallback(inf => { - if(inf !== info){ + if (inf !== info) { toggle.isEnabled.setData(false) } }) diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 378477ccb..5f41c1433 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -55,6 +55,8 @@ import {Tag} from "../Logic/Tags/Tag"; import {And} from "../Logic/Tags/And"; import {SaveButton} from "./Popup/SaveButton"; import {MapillaryLink} from "./BigComponents/MapillaryLink"; +import {CheckBox} from "./Input/Checkboxes"; +import Slider from "./Input/Slider"; export interface SpecialVisualization { funcName: string, @@ -174,22 +176,8 @@ class NearbyImageVis implements SpecialVisualization { const canBeEdited: boolean = !!(id?.match("(node|way|relation)/-?[0-9]+")) const selectedImage = new UIEventSource(undefined); - const nearby = new Lazy(() => { - const alreadyInTheImage = AllImageProviders.LoadImagesFor(tagSource) - const options : NearbyImageOptions & {value}= { - lon, lat, radius: 250, - value: selectedImage, - blacklist: alreadyInTheImage, - towardscenter: false, - maxDaysOld: 365 * 5 - - }; - const slideshow = canBeEdited ? new SelectOneNearbyImage(options) : new NearbyImages(options); - return new Combine([slideshow, new MapillaryLinkVis().constr(state, tagSource, [])]) - }); - - let withEdit: BaseUIElement = nearby; + let saveButton: BaseUIElement = undefined if (canBeEdited) { const confirmText: BaseUIElement = new SubstitutedTranslation(t.confirm, tagSource, state) @@ -212,15 +200,45 @@ class NearbyImageVis implements SpecialVisualization { ) ) }; + saveButton = new SaveButton(selectedImage, state.osmConnection, confirmText, t.noImageSelected) + .onClick(onSave).SetClass("flex justify-end") + } - const saveButton = new SaveButton(selectedImage, state.osmConnection, confirmText, t.noImageSelected) - .onClick(onSave) + const nearby = new Lazy(() => { + const towardsCenter = new CheckBox(t.onlyTowards, false) + + const radiusValue= state?.osmConnection?.GetPreference("nearby-images-radius","300").map(s => Number(s), [], i => ""+i) ?? new UIEventSource(300); + const radius = new Slider(25, 500, {value: + radiusValue, step: 25}) + const alreadyInTheImage = AllImageProviders.LoadImagesFor(tagSource) + const options: NearbyImageOptions & { value } = { + lon, lat, + searchRadius: 500, + shownRadius: radius.GetValue(), + value: selectedImage, + blacklist: alreadyInTheImage, + towardscenter: towardsCenter.GetValue(), + maxDaysOld: 365 * 5 + + }; + const slideshow = canBeEdited ? new SelectOneNearbyImage(options, state) : new NearbyImages(options, state); + const controls = new Combine([towardsCenter, + new Combine([ + new VariableUiElement(radius.GetValue().map(radius => t.withinRadius.Subs({radius}))), radius + ]).SetClass("flex justify-between") + ]).SetClass("flex flex-col"); + return new Combine([slideshow, + controls, + saveButton, + new MapillaryLinkVis().constr(state, tagSource, []).SetClass("mt-6")]) + }); + + let withEdit: BaseUIElement = nearby; + if (canBeEdited) { withEdit = new Combine([ t.hasMatchingPicture, - nearby, - saveButton - .SetClass("flex justify-end") + nearby ]).SetClass("flex flex-col") } diff --git a/assets/svg/filter_disable.svg b/assets/svg/filter_disable.svg new file mode 100644 index 000000000..66e15e9f5 --- /dev/null +++ b/assets/svg/filter_disable.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index a4617ee1e..9cc68c626 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -33,9 +33,7 @@ Use a more readable tab size (opinionated). */ html { - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; + tab-size: 4; } /** @@ -465,7 +463,7 @@ textarea { resize: vertical; } -input::-moz-placeholder, textarea::-moz-placeholder { +input::-webkit-input-placeholder, textarea::-webkit-input-placeholder { opacity: 1; color: #9ca3af; } @@ -872,6 +870,10 @@ video { margin-top: 1rem; } +.mt-6 { + margin-top: 1.5rem; +} + .mt-1 { margin-top: 0.25rem; } @@ -1162,7 +1164,6 @@ video { .w-min { width: -webkit-min-content; - width: -moz-min-content; width: min-content; } @@ -1172,7 +1173,6 @@ video { .w-max { width: -webkit-max-content; - width: -moz-max-content; width: max-content; } @@ -1186,7 +1186,6 @@ video { .min-w-min { min-width: -webkit-min-content; - min-width: -moz-min-content; min-width: min-content; } @@ -1219,18 +1218,21 @@ video { } .transform { - transform: var(--tw-transform); + -webkit-transform: var(--tw-transform); + transform: var(--tw-transform); } @-webkit-keyframes spin { to { - transform: rotate(360deg); + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } @keyframes spin { to { - transform: rotate(360deg); + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } @@ -1452,11 +1454,6 @@ video { border-color: rgba(229, 231, 235, var(--tw-border-opacity)); } -.border-green-500 { - --tw-border-opacity: 1; - border-color: rgba(16, 185, 129, var(--tw-border-opacity)); -} - .border-opacity-50 { --tw-border-opacity: 0.5; } @@ -1774,31 +1771,36 @@ video { .blur { --tw-blur: blur(8px); - filter: var(--tw-filter); + -webkit-filter: var(--tw-filter); + filter: var(--tw-filter); } .drop-shadow { --tw-drop-shadow: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.06)); - filter: var(--tw-filter); + -webkit-filter: var(--tw-filter); + filter: var(--tw-filter); } .invert { --tw-invert: invert(100%); - filter: var(--tw-filter); + -webkit-filter: var(--tw-filter); + filter: var(--tw-filter); } .filter { - filter: var(--tw-filter); + -webkit-filter: var(--tw-filter); + filter: var(--tw-filter); } .\!filter { - filter: var(--tw-filter) !important; + -webkit-filter: var(--tw-filter) !important; + filter: var(--tw-filter) !important; } .transition { - transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, -webkit-transform, -webkit-filter, -webkit-backdrop-filter; transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-transform, -webkit-filter, -webkit-backdrop-filter; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } @@ -1979,16 +1981,19 @@ a { .btn-secondary { background-color: var(--catch-detail-color); - filter: saturate(0.5); + -webkit-filter: saturate(0.5); + filter: saturate(0.5); } .btn-secondary:hover { background-color: var(--catch-detail-color); - filter: unset; + -webkit-filter: unset; + filter: unset; } .btn-disabled { - filter: saturate(0.3); + -webkit-filter: saturate(0.3); + filter: saturate(0.3); cursor: default; } @@ -1998,7 +2003,6 @@ a { .h-min { height: -webkit-min-content; - height: -moz-min-content; height: min-content; } @@ -2008,7 +2012,6 @@ a { .w-min { width: -webkit-min-content; - width: -moz-min-content; width: min-content; } @@ -2180,7 +2183,6 @@ li::marker { .invalid { box-shadow: 0 0 10px #ff5353; height: -webkit-min-content; - height: -moz-min-content; height: min-content; } @@ -2234,11 +2236,13 @@ li::marker { /* This is the animation on the marker to add a new point - it slides through all the possible presets */ from { - transform: translateX(0%); + -webkit-transform: translateX(0%); + transform: translateX(0%); } to { - transform: translateX(calc(-100% + 42px)); + -webkit-transform: translateX(calc(-100% + 42px)); + transform: translateX(calc(-100% + 42px)); } } @@ -2246,18 +2250,21 @@ li::marker { /* This is the animation on the marker to add a new point - it slides through all the possible presets */ from { - transform: translateX(0%); + -webkit-transform: translateX(0%); + transform: translateX(0%); } to { - transform: translateX(calc(-100% + 42px)); + -webkit-transform: translateX(calc(-100% + 42px)); + transform: translateX(calc(-100% + 42px)); } } .hand-drag-animation { -webkit-animation: hand-drag-animation 6s ease-in-out infinite; animation: hand-drag-animation 6s ease-in-out infinite; - transform-origin: 50% 125%; + -webkit-transform-origin: 50% 125%; + transform-origin: 50% 125%; } @-webkit-keyframes hand-drag-animation { @@ -2265,37 +2272,44 @@ li::marker { 0% { opacity: 0; - transform: rotate(-30deg); + -webkit-transform: rotate(-30deg); + transform: rotate(-30deg); } 6% { opacity: 1; - transform: rotate(-30deg); + -webkit-transform: rotate(-30deg); + transform: rotate(-30deg); } 12% { opacity: 1; - transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); } 24% { opacity: 1; - transform: rotate(-00deg); + -webkit-transform: rotate(-00deg); + transform: rotate(-00deg); } 30% { opacity: 1; - transform: rotate(-30deg); + -webkit-transform: rotate(-30deg); + transform: rotate(-30deg); } 36% { opacity: 0; - transform: rotate(-30deg); + -webkit-transform: rotate(-30deg); + transform: rotate(-30deg); } 100% { opacity: 0; - transform: rotate(-30deg); + -webkit-transform: rotate(-30deg); + transform: rotate(-30deg); } } @@ -2304,37 +2318,44 @@ li::marker { 0% { opacity: 0; - transform: rotate(-30deg); + -webkit-transform: rotate(-30deg); + transform: rotate(-30deg); } 6% { opacity: 1; - transform: rotate(-30deg); + -webkit-transform: rotate(-30deg); + transform: rotate(-30deg); } 12% { opacity: 1; - transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); } 24% { opacity: 1; - transform: rotate(-00deg); + -webkit-transform: rotate(-00deg); + transform: rotate(-00deg); } 30% { opacity: 1; - transform: rotate(-30deg); + -webkit-transform: rotate(-30deg); + transform: rotate(-30deg); } 36% { opacity: 0; - transform: rotate(-30deg); + -webkit-transform: rotate(-30deg); + transform: rotate(-30deg); } 100% { opacity: 0; - transform: rotate(-30deg); + -webkit-transform: rotate(-30deg); + transform: rotate(-30deg); } } @@ -2665,7 +2686,6 @@ input { .md\:w-max { width: -webkit-max-content; - width: -moz-max-content; width: max-content; } diff --git a/langs/en.json b/langs/en.json index 7f1133180..23daa33ae 100644 --- a/langs/en.json +++ b/langs/en.json @@ -277,6 +277,7 @@ "dontDelete": "Cancel", "isDeleted": "Deleted", "nearbyPictures": { + "allFiltered": "No images matched your filter", "browseNearby": "Browse nearby images...", "confirm": "The selected image shows {title()}", "hasMatchingPicture": "Does a picture match the object? Select it below", @@ -284,7 +285,10 @@ "loading": "Loading nearby images...", "noImageSelected": "Select an image to link it to the object", "nothingFound": "No nearby images found...", - "title": "Nearby pictures" + "onlyTowards": "Only show pictures which are taken towards this object", + "removeFilters": "Click here to remove the filters", + "title": "Nearby pictures", + "withinRadius": "Only show pictures which are taken within {radius} meter of this object" }, "pleaseLogin": "Please log in to add a picture", "respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.",