diff --git a/assets/layers/gps_location/gps_location.json b/assets/layers/gps_location/gps_location.json index 0a4a689bd..e11ec1d51 100644 --- a/assets/layers/gps_location/gps_location.json +++ b/assets/layers/gps_location/gps_location.json @@ -40,6 +40,30 @@ "centroid" ], "anchor": "center" + }, + { + "marker": [ + { + "color": "--catch-detail-color", + "icon": "direction" + } + ], + "iconSize": { + "render": "0,0", + "mappings": [ + { + "if": "alpha~*", + "then": "40,40" + } + ] + }, + "pitchAlignment": "map", + "rotation": "{alpha}deg", + "location": [ + "point", + "centroid" + ], + "anchor": "center" } ], "lineRendering": [] diff --git a/assets/layers/icons/icons.json b/assets/layers/icons/icons.json index 8d796b500..3acfb3cac 100644 --- a/assets/layers/icons/icons.json +++ b/assets/layers/icons/icons.json @@ -302,6 +302,14 @@ "condition": "_favourite=yes", "icon": "circle:white;heart:red", "metacondition": "__showTimeSensitiveIcons!=no" + }, + { + "id": "direction", + "labels": [ + "defaults", + "in_favourite" + ], + "render": "{direction_indicator()}" } ] } diff --git a/assets/layers/playground/playground.json b/assets/layers/playground/playground.json index cdec3d747..4324a58df 100644 --- a/assets/layers/playground/playground.json +++ b/assets/layers/playground/playground.json @@ -366,7 +366,7 @@ }, "freeform": { "key": "min_age", - "type": "pnat" + "type": "nat" }, "id": "playground-min_age" }, diff --git a/langs/en.json b/langs/en.json index f5ba8ef11..6676b04e0 100644 --- a/langs/en.json +++ b/langs/en.json @@ -399,12 +399,34 @@ "useSearchForMore": "Use the search function to search within {total} more values…", "visualFeedback": { "closestFeaturesAre": "{n} features within viewport.", + "directionsAbsolute": { + "E": "east", + "N": "north", + "NE": "northeast", + "NW": "northwest", + "S": "south", + "SE": "southeast", + "SW": "southwest", + "W": "west" + }, + "directionsRelative": { + "behind": "on your back", + "left": "left", + "right": "right", + "sharp_left": "sharply left", + "sharp_right": "sharply right", + "slight_left": "slightly left", + "slight_right": "slightly right", + "straight": "straight ahead" + }, "east": "Moving east", + "fromGps": "{distance} {direction} of your location", + "fromMapCenter": "{distance} {direction} of the map center", "in": "Zooming in to level {z}", "islocked": "View locked to your GPS-location, moving disabled. Press the geolocation button to unlock.", "locked": "View is now locked to your GPS-location, moving disabled.", "navigation": "Use arrow keys to move the map, press space to select the closest feature. Press a number to select locations further away.", - "noCloseFeatures": "No features in view", + "noCloseFeatures": "No features in view.", "north": "Moving north", "oneFeatureInView": "One feature within viewport.", "out": "Zooming out to level {z}", @@ -636,12 +658,15 @@ "reviews": { "affiliated_reviewer_warning": "(Affiliated review)", "attribution": "Reviews are powered by Mangrove Reviews and are available under CC-BY 4.0.", + "averageRating": "Average rating of {n} stars", "i_am_affiliated": "I am affiliated with this object", "i_am_affiliated_explanation": "Check if you are an owner, creator, employee, …", "name_required": "A name is required in order to display and create reviews", "no_reviews_yet": "There are no reviews yet. Be the first to write one and help open data and the business!", "question": "How would you rate {title()}?", "question_opinion": "How was your experience?", + "rate": "Rate {n} stars", + "rated": "Rated {n} stars", "reviewing_as": "Reviewing as {nickname}", "reviewing_as_anonymous": "Reviewing as anonymous", "save": "Save", diff --git a/package.json b/package.json index aa350f9ae..eedb4a2d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.36.7", + "version": "0.36.8", "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", "bugs": "https://github.com/pietervdvn/MapComplete/issues", diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 0f157c68c..836ab5ba9 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -1347,6 +1347,10 @@ video { appearance: none; } +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -1450,14 +1454,26 @@ video { row-gap: 0.5rem; } +.gap-x-1 { + -webkit-column-gap: 0.25rem; + column-gap: 0.25rem; +} + .gap-x-2 { -webkit-column-gap: 0.5rem; column-gap: 0.5rem; } -.gap-x-1 { - -webkit-column-gap: 0.25rem; - column-gap: 0.25rem; +.space-x-0\.5 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.125rem * var(--tw-space-x-reverse)); + margin-left: calc(0.125rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-0 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0px * var(--tw-space-x-reverse)); + margin-left: calc(0px * calc(1 - var(--tw-space-x-reverse))); } .space-x-1 > :not([hidden]) ~ :not([hidden]) { @@ -1466,6 +1482,18 @@ video { margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); } +.space-y-0\.5 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.125rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.125rem * var(--tw-space-y-reverse)); +} + +.space-y-0 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0px * var(--tw-space-y-reverse)); +} + .space-x-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.5rem * var(--tw-space-x-reverse)); @@ -1554,6 +1582,10 @@ video { text-overflow: clip; } +.break-words { + overflow-wrap: break-word; +} + .break-all { word-break: break-all; } @@ -1695,11 +1727,6 @@ video { border-color: rgb(219 234 254 / var(--tw-border-opacity)); } -.border-red-500 { - --tw-border-opacity: 1; - border-color: rgb(239 68 68 / var(--tw-border-opacity)); -} - .border-gray-300 { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity)); diff --git a/src/Logic/Actors/GeoLocationHandler.ts b/src/Logic/Actors/GeoLocationHandler.ts index 68efc803d..152b12979 100644 --- a/src/Logic/Actors/GeoLocationHandler.ts +++ b/src/Logic/Actors/GeoLocationHandler.ts @@ -10,6 +10,7 @@ import { GeoOperations } from "../GeoOperations" import { OsmTags } from "../../Models/OsmFeature" import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource" import { MapProperties } from "../../Models/MapProperties" +import { Orientation } from "../../Sensors/Orientation" /** * The geolocation-handler takes a map-location and a geolocation state. @@ -128,10 +129,10 @@ export default class GeoLocationHandler { } // We check that the GPS location is not out of bounds - const bounds = this.mapProperties.maxbounds.data + const bounds: BBox = this.mapProperties.maxbounds.data if (bounds !== undefined) { // B is an array with our lock-location - const inRange = new BBox(bounds).contains([newLocation.longitude, newLocation.latitude]) + const inRange = bounds.contains([newLocation.longitude, newLocation.latitude]) if (!inRange) { return } @@ -167,6 +168,9 @@ export default class GeoLocationHandler { altitude: location.altitude, altitudeAccuracy: location.altitudeAccuracy, heading: location.heading, + alpha: Orientation.singleton.gotMeasurement.data + ? "" + Orientation.singleton.alpha.data + : undefined, } i++ diff --git a/src/Logic/Web/MangroveReviews.ts b/src/Logic/Web/MangroveReviews.ts index 8bcfe3807..49429f5ff 100644 --- a/src/Logic/Web/MangroveReviews.ts +++ b/src/Logic/Web/MangroveReviews.ts @@ -55,8 +55,9 @@ export default class FeatureReviews { private static readonly _featureReviewsCache: Record = {} public readonly subjectUri: Store public readonly average: Store - private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store })[]> = - new UIEventSource([]) + private readonly _reviews: UIEventSource< + (Review & { kid: string; signature: string; madeByLoggedInUser: Store })[] + > = new UIEventSource([]) public readonly reviews: Store<(Review & { madeByLoggedInUser: Store })[]> = this._reviews private readonly _lat: number @@ -176,11 +177,15 @@ export default class FeatureReviews { ...review, } const keypair: CryptoKeyPair = this._identity.keypair.data - console.log(r) const jwt = await MangroveReviews.signReview(keypair, r) - console.log("Signed:", jwt) + const kid = await MangroveReviews.publicToPem(keypair.publicKey) await MangroveReviews.submitReview(jwt) - this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) }) + this._reviews.data.push({ + ...r, + kid, + signature: jwt, + madeByLoggedInUser: new ImmutableStore(true), + }) this._reviews.ping() } @@ -189,7 +194,7 @@ export default class FeatureReviews { * @param reviews * @private */ - private addReviews(reviews: { payload: Review; kid: string }[]) { + private addReviews(reviews: { payload: Review; kid: string; signature: string }[]) { const self = this const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion)) @@ -199,7 +204,6 @@ export default class FeatureReviews { try { const url = new URL(review.sub) - console.log("URL is", url) if (url.protocol === "geo:") { const coordinate = <[number, number]>( url.pathname.split(",").map((n) => Number(n)) @@ -222,6 +226,8 @@ export default class FeatureReviews { } self._reviews.data.push({ ...review, + kid: reviewData.kid, + signature: reviewData.signature, madeByLoggedInUser: this._identity.key_id.map((user_key_id) => { return reviewData.kid === user_key_id }), diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts index bf2e0e5cc..326087e09 100644 --- a/src/Models/Constants.ts +++ b/src/Models/Constants.ts @@ -99,6 +99,7 @@ export default class Constants { * In seconds */ static zoomToLocationTimeout = 15 + public static readonly viewportCenterCloseToGpsCutoff: number = 20 private static readonly config = (() => { const defaultConfig = packagefile.config return { ...defaultConfig, ...extraconfig } diff --git a/src/Models/ThemeConfig/Conversion/PrepareLayer.ts b/src/Models/ThemeConfig/Conversion/PrepareLayer.ts index 99796ba08..917c98873 100644 --- a/src/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/src/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -576,6 +576,7 @@ export class AddEditingElements extends DesugaringStep { "last_edit", "favourite_state", "all_tags", + "qr_code", ] private readonly _desugaring: DesugaringContext @@ -657,6 +658,13 @@ export class AddEditingElements extends DesugaringStep { }) } + if (!usedSpecialFunctions.has("qr_code")) { + json.tagRenderings.push({ + id: "qr_code", + render: { "*": "{qr_code()}" }, + }) + } + if (!usedSpecialFunctions.has("all_tags")) { const trc: QuestionableTagRenderingConfigJson = { id: "all-tags", diff --git a/src/Models/ThemeConfig/TagRenderingConfig.ts b/src/Models/ThemeConfig/TagRenderingConfig.ts index b8f09527c..f2d8367a8 100644 --- a/src/Models/ThemeConfig/TagRenderingConfig.ts +++ b/src/Models/ThemeConfig/TagRenderingConfig.ts @@ -631,6 +631,10 @@ export default class TagRenderingConfig { * , "testcase") * config.constructChangeSpecification(undefined, undefined, [false, true, false], {amenity: "public_bookcase"}) // => new And([new Tag("books","adults")]) * + * const config = new TagRenderingConfig({"id":"capacity", "render": "Fits {capcity} books",freeform: {"key":"capacity",type:"pnat"} }) + * config.constructChangeSpecification("", undefined, undefined, {}) // => undefined + * config.constructChangeSpecification("5", undefined, undefined, {}).optimize() // => new Tag("capacity", "5") + * * @param freeformValue The freeform value which will be applied as 'freeform.key'. Ignored if 'freeform.key' is not set * * @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform diff --git a/src/Sensors/Orientation.ts b/src/Sensors/Orientation.ts index df789a2c8..67c14cd82 100644 --- a/src/Sensors/Orientation.ts +++ b/src/Sensors/Orientation.ts @@ -49,7 +49,7 @@ export class Orientation { if (rotateAlpha) { this._animateFakeMeasurements = true Stores.Chronic(25).addCallback((date) => { - this.alpha.setData((date.getTime() / 100) % 360) + this.alpha.setData((date.getTime() / 50) % 360) if (!this._animateFakeMeasurements) { return true } diff --git a/src/UI/Base/DirectionIndicator.svelte b/src/UI/Base/DirectionIndicator.svelte index 6c2741503..27b6b6d7f 100644 --- a/src/UI/Base/DirectionIndicator.svelte +++ b/src/UI/Base/DirectionIndicator.svelte @@ -8,20 +8,25 @@ import { GeoOperations } from "../../Logic/GeoOperations" import { Store } from "../../Logic/UIEventSource" import type { Feature } from "geojson" - import ThemeViewState from "../../Models/ThemeViewState" import Compass_arrow from "../../assets/svg/Compass_arrow.svelte" import { twMerge } from "tailwind-merge" import { Orientation } from "../../Sensors/Orientation" + import Translations from "../i18n/Translations" + import Constants from "../../Models/Constants" + import Locale from "../i18n/Locale" + import { ariaLabelStore } from "../../Utils/ariaLabel" + import type { SpecialVisualizationState } from "../SpecialVisualization" - export let state: ThemeViewState + export let state: SpecialVisualizationState export let feature: Feature + export let size = "w-8 h-8" let fcenter = GeoOperations.centerpointCoordinates(feature) // Bearing and distance relative to the map center let bearingAndDist: Store<{ bearing: number; dist: number }> = state.mapProperties.location.map( (l) => { let mapCenter = [l.lon, l.lat] - let bearing = Math.round(GeoOperations.bearing(fcenter, mapCenter)) + let bearing = Math.round(GeoOperations.bearing(mapCenter, fcenter)) let dist = Math.round(GeoOperations.distanceBetween(fcenter, mapCenter)) return { bearing, dist } }, @@ -29,19 +34,87 @@ let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD(coordinate => { return GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter) }) - let compass = Orientation.singleton.alpha.map(compass => compass ?? 0) - export let size = "w-8 h-8" + let compass = Orientation.singleton.alpha + + let relativeDirections = Translations.t.general.visualFeedback.directionsRelative + let absoluteDirections = Translations.t.general.visualFeedback.directionsAbsolute + + let closeToCurrentLocation = state.geolocation.geolocationState.currentGPSLocation.map(gps => { + if (!gps) { + return false + } + let l = state.mapProperties.location.data + let mapCenter = [l.lon, l.lat] + const dist = GeoOperations.distanceBetween([gps.longitude, gps.latitude], mapCenter) + return dist < Constants.viewportCenterCloseToGpsCutoff + }, + [state.mapProperties.location], + ) + let labelFromCenter: Store = bearingAndDist.mapD(({ bearing, dist }) => { + const distHuman = GeoOperations.distanceToHuman(dist) + const lang = Locale.language.data + const t = absoluteDirections[GeoOperations.bearingToHuman(bearing)] + const mainTr = Translations.t.general.visualFeedback.fromMapCenter.Subs({ + distance: distHuman, + direction: t.textFor(lang), + }) + return mainTr.textFor(lang) + }, [compass, Locale.language]) + + + // Bearing and distance relative to the map center + let bearingAndDistGps: Store<{ + bearing: number; + dist: number + } | undefined> = state.geolocation.geolocationState.currentGPSLocation.mapD( + ({ longitude, latitude }) => { + let gps = [longitude, latitude] + let bearing = Math.round(GeoOperations.bearing(gps, fcenter)) + let dist = Math.round(GeoOperations.distanceBetween(fcenter, gps)) + return { bearing, dist } + }, + ) + let labelFromGps: Store = bearingAndDistGps.mapD(({ bearing, dist }) => { + const distHuman = GeoOperations.distanceToHuman(dist) + const lang = Locale.language.data + let bearingHuman: string + if (compass.data !== undefined) { + console.log("compass:", compass.data) + const bearingRelative = bearing - compass.data + const t = relativeDirections[GeoOperations.bearingToHumanRelative(bearingRelative)] + bearingHuman = t.textFor(lang) + } else { + bearingHuman = absoluteDirections[GeoOperations.bearingToHuman(bearing)].textFor(lang) + } + const mainTr = Translations.t.general.visualFeedback.fromGps.Subs({ + distance: distHuman, + direction: bearingHuman, + }) + return mainTr.textFor(lang) + }, [compass, Locale.language]) + + let label = labelFromCenter.map(labelFromCenter => { + if (labelFromGps.data !== undefined) { + if(closeToCurrentLocation.data){ + return labelFromGps.data + } + return labelFromCenter + ", " + labelFromGps.data + } + return labelFromCenter + }, [labelFromGps]) + function focusMap(){ + state.mapProperties.location.setData({ lon: fcenter[0], lat: fcenter[1] }) + } -
-
- {GeoOperations.distanceToHuman($bearingAndDist.dist)} + diff --git a/src/UI/Base/Tr.svelte b/src/UI/Base/Tr.svelte index da694a06a..7de9266ec 100644 --- a/src/UI/Base/Tr.svelte +++ b/src/UI/Base/Tr.svelte @@ -11,11 +11,12 @@ export let cls: string = "" // Text for the current language let txt: Store = t?.current + $: {txt = t?.current} {#if $txt} - + {/if} diff --git a/src/UI/BigComponents/MapCenterDetails.svelte b/src/UI/BigComponents/MapCenterDetails.svelte index df4bde637..a985a3fc0 100644 --- a/src/UI/BigComponents/MapCenterDetails.svelte +++ b/src/UI/BigComponents/MapCenterDetails.svelte @@ -4,12 +4,16 @@ import ThemeViewState from "../../Models/ThemeViewState" import Tr from "../Base/Tr.svelte" import Translations from "../i18n/Translations" + import { Orientation } from "../../Sensors/Orientation" + import { Translation } from "../i18n/Translation" + import Constants from "../../Models/Constants" /** * Indicates how far away the viewport center is from the current user location */ export let state: ThemeViewState const t = Translations.t.general.visualFeedback + const relativeDir = t.directionsRelative let map = state.mapProperties let currentLocation = state.geolocation.geolocationState.currentGPSLocation @@ -23,14 +27,29 @@ const distanceInMeters = Math.round(GeoOperations.distanceBetween(gps, mapCenter)) const distance = GeoOperations.distanceToHuman(distanceInMeters) const bearing = Math.round(GeoOperations.bearing(gps, mapCenter)) - return { distance, bearing, distanceInMeters } + const bearingDirection = GeoOperations.bearingToHuman(bearing) + return { distance, bearing, distanceInMeters, bearingDirection } }, [currentLocation]) + let hasCompass = Orientation.singleton.gotMeasurement + let compass = Orientation.singleton.alpha + let relativeBearing: Store<{distance: string, bearing: Translation}> = + compass.mapD(compass => { + const bearing: Translation = relativeDir[GeoOperations.bearingToHumanRelative(distanceToCurrentLocation.data.bearing - compass)] + return {bearing, distance: distanceToCurrentLocation.data.distance} + }, [distanceToCurrentLocation]) + let viewportCenterDetails = Translations.DynamicSubstitute(t.viewportCenterDetails, relativeBearing) + let viewportCenterDetailsAbsolute = Translations.DynamicSubstitute(t.viewportCenterDetails, distanceToCurrentLocation.map(({distance, bearing}) => { + return {distance, bearing: t.directionsAbsolute[GeoOperations.bearingToHuman(bearing)]} + })) + {#if $currentLocation !== undefined} - {#if $distanceToCurrentLocation.distanceInMeters < 20} + {#if $distanceToCurrentLocation.distanceInMeters < Constants.viewportCenterCloseToGpsCutoff} + {:else if $hasCompass} + {$viewportCenterDetails} {:else} - + {$viewportCenterDetailsAbsolute} {/if} {/if} diff --git a/src/UI/BigComponents/ReverseGeocoding.svelte b/src/UI/BigComponents/ReverseGeocoding.svelte index 77877bdc2..a247d1f17 100644 --- a/src/UI/BigComponents/ReverseGeocoding.svelte +++ b/src/UI/BigComponents/ReverseGeocoding.svelte @@ -55,11 +55,10 @@ {#if currentLocation} {/if} diff --git a/src/UI/BigComponents/Summary.svelte b/src/UI/BigComponents/Summary.svelte index e3e446f55..06a24c0d5 100644 --- a/src/UI/BigComponents/Summary.svelte +++ b/src/UI/BigComponents/Summary.svelte @@ -4,6 +4,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" import DirectionIndicator from "../Base/DirectionIndicator.svelte" + import ThemeViewState from "../../Models/ThemeViewState" export let state: SpecialVisualizationState export let feature: Feature @@ -14,11 +15,14 @@ - + + + {#if i !== undefined} {i + 1}   {/if} - + + diff --git a/src/UI/BigComponents/VisualFeedbackPanel.svelte b/src/UI/BigComponents/VisualFeedbackPanel.svelte index 264e02f7d..77199c547 100644 --- a/src/UI/BigComponents/VisualFeedbackPanel.svelte +++ b/src/UI/BigComponents/VisualFeedbackPanel.svelte @@ -32,7 +32,7 @@ lastAction.stabilized(750).addCallbackAndRunD((_) => lastAction.setData(undefined)) - diff --git a/src/UI/Favourites/FavouriteSummary.svelte b/src/UI/Favourites/FavouriteSummary.svelte index e423f81ec..9f2cdb9d9 100644 --- a/src/UI/Favourites/FavouriteSummary.svelte +++ b/src/UI/Favourites/FavouriteSummary.svelte @@ -2,9 +2,9 @@ import type { SpecialVisualizationState } from "../SpecialVisualization" import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" import type { Feature } from "geojson" - import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource" + import { UIEventSource } from "../../Logic/UIEventSource" import { GeoOperations } from "../../Logic/GeoOperations" - import Center from "../../assets/svg/Center.svelte" + import DirectionIndicator from "../Base/DirectionIndicator.svelte" export let feature: Feature let properties: Record = feature.properties @@ -30,11 +30,6 @@ center() } - const coord = GeoOperations.centerpointCoordinates(feature) - const distance = state.mapProperties.location.stabilized(500).mapD(({ lon, lat }) => { - let meters = Math.round(GeoOperations.distanceBetween(coord, [lon, lat])) - return GeoOperations.distanceToHuman(meters) - }) const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"] @@ -57,7 +52,7 @@ class="title-icons links-as-button flex flex-wrap items-center gap-x-0.5 self-end justify-self-end p-1 pt-0.5 sm:pt-1" > {#each favConfig.titleIcons as titleIconConfig} - {#if titleIconBlacklist.indexOf(titleIconConfig.id) < 0 && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ...properties, ...state.userRelatedState.preferencesAsTags.data } ) ?? true) && titleIconConfig.IsKnown(properties)} + {#if titleIconBlacklist.indexOf(titleIconConfig.id) < 0 && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...properties, ...state.userRelatedState.preferencesAsTags.data }) ?? true) && titleIconConfig.IsKnown(properties)}
center()}> -
- -
- {$distance} -
+
{/if} diff --git a/src/UI/Favourites/Favourites.svelte b/src/UI/Favourites/Favourites.svelte index 28b9b0523..9cc574a60 100644 --- a/src/UI/Favourites/Favourites.svelte +++ b/src/UI/Favourites/Favourites.svelte @@ -48,7 +48,7 @@
console.log("Got keypress", e)}> - + {#each $favourites as feature (feature.properties.id)} diff --git a/src/UI/InputElement/ValidatedInput.svelte b/src/UI/InputElement/ValidatedInput.svelte index f5ccf525f..37c55b744 100644 --- a/src/UI/InputElement/ValidatedInput.svelte +++ b/src/UI/InputElement/ValidatedInput.svelte @@ -57,7 +57,11 @@ validator = Validators.get(type ?? "string") _placeholder = placeholder ?? validator?.getPlaceholder() ?? type - feedback?.setData(validator?.getFeedback(_value.data, getCountry)) + if(_value.data?.length > 0){ + feedback?.setData(validator?.getFeedback(_value.data, getCountry)) + }else{ + feedback?.setData(undefined) + } initValueAndDenom() } @@ -65,9 +69,14 @@ function setValues() { // Update the value stores const v = _value.data - if (!validator?.isValid(v, getCountry) || v === "") { + if(v === ""){ + value.setData(undefined) + feedback.setData(undefined) + return + } + if (!validator?.isValid(v, getCountry)) { feedback?.setData(validator?.getFeedback(v, getCountry)) - value.setData("") + value.setData(undefined) return } diff --git a/src/UI/InputElement/Validators/NatValidator.ts b/src/UI/InputElement/Validators/NatValidator.ts index fbc77758c..665c9cc03 100644 --- a/src/UI/InputElement/Validators/NatValidator.ts +++ b/src/UI/InputElement/Validators/NatValidator.ts @@ -16,15 +16,22 @@ export default class NatValidator extends IntValidator { return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 } + /** + * + * const validator = new NatValidator() + * validator.getFeedback(-4).textFor("en") // => "This number should be positive" + */ getFeedback(s: string): Translation { + console.log("Getting feedback for", s) + const n = Number(s) + if (!isNaN(n) && n < 0) { + return Translations.t.validation.nat.mustBePositive + } const spr = super.getFeedback(s) if (spr !== undefined) { return spr } - const n = Number(s) - if (n < 0) { - return Translations.t.validation.nat.mustBePositive - } + return undefined } } diff --git a/src/UI/Map/DynamicMarker.svelte b/src/UI/Map/DynamicMarker.svelte index 768cbfac7..781d537a7 100644 --- a/src/UI/Map/DynamicMarker.svelte +++ b/src/UI/Map/DynamicMarker.svelte @@ -3,16 +3,20 @@ import { ImmutableStore, Store } from "../../Logic/UIEventSource" import DynamicIcon from "./DynamicIcon.svelte" import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" + import { Orientation } from "../../Sensors/Orientation" /** * Renders a 'marker', which consists of multiple 'icons' */ - export let marker: IconConfig[] = config?.marker + export let marker: IconConfig[] export let tags: Store> export let rotation: TagRenderingConfig = undefined - let _rotation = rotation + let _rotation: Store = rotation ? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt) - : new ImmutableStore(0) + : new ImmutableStore("0deg") + if(rotation?.render?.txt === "{alpha}deg"){ + _rotation = Orientation.singleton.alpha.map(alpha => alpha ? (alpha)+"deg" : "0deg ") + } {#if marker && marker} diff --git a/src/UI/Map/Icon.svelte b/src/UI/Map/Icon.svelte index c970c98e4..dbbcc4162 100644 --- a/src/UI/Map/Icon.svelte +++ b/src/UI/Map/Icon.svelte @@ -27,6 +27,7 @@ import Confirm from "../../assets/svg/Confirm.svelte" import Not_found from "../../assets/svg/Not_found.svelte" import { twMerge } from "tailwind-merge" + import Direction_gradient from "../../assets/svg/Direction_gradient.svelte" /** * Renders a single icon. @@ -100,6 +101,8 @@ {:else if icon === "confirm"} + {:else if icon === "direction"} + {:else if icon === "not_found"} {:else} diff --git a/src/UI/Map/ShowDataLayer.ts b/src/UI/Map/ShowDataLayer.ts index 19b7fdd13..75c7213b0 100644 --- a/src/UI/Map/ShowDataLayer.ts +++ b/src/UI/Map/ShowDataLayer.ts @@ -171,6 +171,7 @@ class PointRenderingLayer { store .map((tags) => this._config.rotationAlignment.GetRenderValue(tags).Subs(tags).txt) .addCallbackAndRun((pitchAligment) => marker.setRotationAlignment(pitchAligment)) + if (feature.geometry.type === "Point") { // When the tags get 'pinged', check that the location didn't change store.addCallbackAndRunD(() => { diff --git a/src/UI/Reviews/SingleReview.svelte b/src/UI/Reviews/SingleReview.svelte index af5855f0d..4f92f2f56 100644 --- a/src/UI/Reviews/SingleReview.svelte +++ b/src/UI/Reviews/SingleReview.svelte @@ -4,13 +4,11 @@ import StarsBar from "./StarsBar.svelte" import Translations from "../i18n/Translations" import Tr from "../Base/Tr.svelte" + import { ariaLabel } from "../../Utils/ariaLabel" - export let review: Review & { madeByLoggedInUser: Store } + export let review: Review & { kid: string,signature: string, madeByLoggedInUser: Store } let name = review.metadata.nickname - name ??= (review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "").trim() - if (name.length === 0) { - name = "Anonymous" - } + name ??= ((review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "")).trim() let d = new Date() d.setTime(review.iat * 1000) let date = d.toDateString() @@ -19,18 +17,32 @@
{#if review.opinion} - {review.opinion} + {/if} {#if review.metadata.is_affiliated} diff --git a/src/UI/Reviews/StarElement.svelte b/src/UI/Reviews/StarElement.svelte index 09b0e56ce..f91f21bfe 100644 --- a/src/UI/Reviews/StarElement.svelte +++ b/src/UI/Reviews/StarElement.svelte @@ -1,31 +1,32 @@ -
dispatch("click", { score: getScore(e) })} - on:mousemove={(e) => dispatch("hover", { score: getScore(e) })} -> +{#if readonly} {#if score >= cutoff} {:else if score + 10 >= cutoff} @@ -33,4 +34,22 @@ {:else} {/if} -
+ +{:else} + +{/if} diff --git a/src/UI/Reviews/StarsBar.svelte b/src/UI/Reviews/StarsBar.svelte index d2f13a824..f52a2d2ef 100644 --- a/src/UI/Reviews/StarsBar.svelte +++ b/src/UI/Reviews/StarsBar.svelte @@ -1,5 +1,4 @@ {#if score !== undefined}
{#each cutoffs as cutoff, i} - + {/each}
{/if} diff --git a/src/UI/Reviews/StarsBarIcon.svelte b/src/UI/Reviews/StarsBarIcon.svelte index 72f9d8f73..7cd402d22 100644 --- a/src/UI/Reviews/StarsBarIcon.svelte +++ b/src/UI/Reviews/StarsBarIcon.svelte @@ -1,10 +1,16 @@ {#if $score !== undefined && $score !== null} - +
+ +
{/if} diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index a898ac226..b86ffe56a 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -19,6 +19,7 @@ import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" import { OsmTags } from "../Models/OsmFeature" import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource" import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider" +import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" /** * The state needed to render a special Visualisation. @@ -87,6 +88,7 @@ export interface SpecialVisualizationState { readonly imageUploadManager: ImageUploadManager readonly previewedImage: UIEventSource + readonly geolocation: GeoLocationHandler } export interface SpecialVisualization { diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 33148e62d..53926d425 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -85,6 +85,9 @@ import { Unit } from "../Models/Unit" import Link from "./Base/Link.svelte" import OrientationDebugPanel from "./Debug/OrientationDebugPanel.svelte" import MaprouletteSetStatus from "./MapRoulette/MaprouletteSetStatus.svelte" +import DirectionIndicator from "./Base/DirectionIndicator.svelte" +import Img from "./Base/Img" +import Qr from "../Utils/Qr" class NearbyImageVis implements SpecialVisualization { // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests @@ -1539,6 +1542,43 @@ export default class SpecialVisualizations { }) }, }, + { + funcName: "direction_indicator", + args: [], + needsUrls: [], + docs: "Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object", + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + argument: string[], + feature: Feature, + layer: LayerConfig + ): BaseUIElement { + return new SvelteUIElement(DirectionIndicator, { state, feature }) + }, + }, + { + funcName: "qr_code", + args: [], + needsUrls: [], + docs: "Generates a QR-code to share the selected object", + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + argument: string[], + feature: Feature, + layer: LayerConfig + ): BaseUIElement { + const url = + window.location.protocol + + "//" + + window.location.host + + window.location.pathname + + "#" + + feature.properties.id + return new Img(new Qr(url).toImageElement(75)).SetStyle("width: 75px") + }, + }, ] specialVisualizations.push(new AutoApplyButton(specialVisualizations)) diff --git a/src/UI/i18n/Translations.ts b/src/UI/i18n/Translations.ts index 8ed7f9dad..e6e296e68 100644 --- a/src/UI/i18n/Translations.ts +++ b/src/UI/i18n/Translations.ts @@ -4,6 +4,9 @@ import BaseUIElement from "../BaseUIElement" import CompiledTranslations from "../../assets/generated/CompiledTranslations" import LanguageUtils from "../../Utils/LanguageUtils" import { ClickableToggle } from "../Input/Toggle" +import { Store } from "../../Logic/UIEventSource" +import Locale from "./Locale" +import { Utils } from "../../Utils" export default class Translations { static readonly t: Readonly = CompiledTranslations.t @@ -130,6 +133,29 @@ export default class Translations { } } + public static DynamicSubstitute>( + translation: TypedTranslation, + t: Store + ): Store { + return Locale.language.map( + (lang) => { + const tags: Record = {} + for (const k in t.data) { + let v = t.data[k] + if (!v) { + continue + } + if (v["textFor"] !== undefined) { + v = v["textFor"](lang) + } + tags[k] = v + } + return Utils.SubstituteKeys(translation.textFor(lang), t.data) + }, + [t] + ) + } + static isProbablyATranslation(transl: any) { if (!transl || typeof transl !== "object") { return false diff --git a/src/Utils/Qr.ts b/src/Utils/Qr.ts index 55be7626a..2a48e6d3c 100644 --- a/src/Utils/Qr.ts +++ b/src/Utils/Qr.ts @@ -4,23 +4,20 @@ import Qrcode from "qrcode-generator" * Creates a QR-code as Blob */ export default class Qr { - private _textToShow: string + private readonly _textToShow: string constructor(textToShow: string) { this._textToShow = textToShow } public toImageElement(totalSize: number): string { - console.log("Creating a QR code for", this._textToShow) const typeNumber = 0 const errorCorrectionLevel = "L" const qr = Qrcode(typeNumber, errorCorrectionLevel) qr.addData(this._textToShow) qr.make() const moduleCount = qr.getModuleCount() - const img = document.createElement("img") const cellSize = Math.round(totalSize / moduleCount) - console.log("Cellsize", cellSize) return qr.createDataURL(cellSize) } }