From 6da72b80ef9ed7ce62f4873dafaaf44885d3c6cb Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 21 Dec 2023 17:36:43 +0100 Subject: [PATCH] More work on A11y --- assets/layers/usersettings/usersettings.json | 3 +- langs/da.json | 10 +-- langs/en.json | 9 +- langs/nl.json | 6 +- public/css/index-tailwind-output.css | 14 ++- src/Logic/BBox.ts | 4 + .../Sources/NearbyFeatureSource.ts | 37 +++++--- src/Logic/GeoOperations.ts | 85 ++++++++++++++++++- src/Logic/State/UserSettingsMetaTagging.ts | 48 +++-------- src/Logic/UIEventSource.ts | 30 ++++--- .../QuestionableTagRenderingConfigJson.ts | 6 ++ .../Json/TagRenderingConfigJson.ts | 1 + src/Models/ThemeConfig/TagRenderingConfig.ts | 13 ++- src/Models/ThemeViewState.ts | 25 +++++- src/UI/Base/DirectionIndicator.svelte | 47 ++++++++++ src/UI/Base/Hotkeys.ts | 3 +- src/UI/Base/Link.svelte | 1 + src/UI/BigComponents/MapCenterDetails.svelte | 36 ++++++++ src/UI/BigComponents/ReverseGeocoding.svelte | 8 +- .../BigComponents/SelectedElementTitle.svelte | 3 + src/UI/BigComponents/Summary.svelte | 36 ++------ .../BigComponents/VisualFeedbackPanel.svelte | 38 ++++++--- src/UI/Favourites/FavouriteSummary.svelte | 10 +-- src/UI/OpeningHours/OpeningHours.ts | 20 +++-- src/UI/Reviews/StarElement.svelte | 3 +- src/UI/Reviews/StarsBar.svelte | 5 +- src/UI/ThemeViewGUI.svelte | 83 +++++++++--------- src/Utils/ariaLabel.ts | 23 ++--- 28 files changed, 398 insertions(+), 209 deletions(-) create mode 100644 src/UI/Base/DirectionIndicator.svelte create mode 100644 src/UI/BigComponents/MapCenterDetails.svelte diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index a98a6536d..694d089fe 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -139,7 +139,8 @@ "condition": "_theme:backgroundLayer=", "mappings": [ { - "if": "mapcomplete-preferred-background-layer=", + "if": "mapcomplete-preferred-background-layer=default", + "alsoShowIf": "mapcomplete-preferred-background-layer=", "then": { "en": "Use the default background layer", "ca": "Utilitzeu la capa de fons predeterminada", diff --git a/langs/da.json b/langs/da.json index 8b4e75f26..add783f4b 100644 --- a/langs/da.json +++ b/langs/da.json @@ -94,10 +94,10 @@ "backToSelect": "Vælg en anden kategori", "confirmButton": "Tilføj en {category}
Din tilføjelse er synlig for alle
", "confirmLocation": "Bekræft dette sted", - "confirmTitle": "Tilføj en {titel}?", + "confirmTitle": "Tilføj en {title}?", "disableFilters": "Slå alle filtre fra", "disableFiltersExplanation": "Nogle elementer kan være skjult af et filter", - "enableLayer": "Aktivér lag {navn}", + "enableLayer": "Aktivér lag {name}", "hasBeenImported": "Punktet er allerede importeret", "import": { "hasBeenImported": "Objektet blev importeret", @@ -125,8 +125,8 @@ "isApplied": "Ændringerne er anvendt" }, "attribution": { - "attributionBackgroundLayer": "Det nuværende baggrundslag er {navn}", - "attributionBackgroundLayerWithCopyright": "Det nuværende baggrundslag er [navn}: {copyright}", + "attributionBackgroundLayer": "Det nuværende baggrundslag er {name}", + "attributionBackgroundLayerWithCopyright": "Det nuværende baggrundslag er {name}: {copyright}", "attributionContent": "

Alle data leveres af OpenStreetMap, frit genanvendelige under Open DataBase Licensen.

", "attributionTitle": "Meddelelse om tilskrivning", "codeContributionsBy": "MapComplete er lavet af {contributors} og {hiddenCount} flere bidragsydere", @@ -253,7 +253,7 @@ "pickLanguage": "Vælg et sprog: ", "poweredByOsm": "Drevet af OpenStreetMap", "questionBox": { - "answeredMultiple": "Du besvarede [answered} spørgsmål", + "answeredMultiple": "Du besvarede {answered} spørgsmål", "answeredMultipleSkippedMultiple": "Du besvarede {answered} spørgsmål og sprang over {skipped} spørgsmål", "answeredMultipleSkippedOne": "Du besvarede {answered} spørgsmål og sprang over ét spørgsmål", "answeredOne": "Du besvarede ét spørgsmål", diff --git a/langs/en.json b/langs/en.json index 0860c347c..a58063266 100644 --- a/langs/en.json +++ b/langs/en.json @@ -398,17 +398,20 @@ "useSearch": "Use the search above to see presets", "useSearchForMore": "Use the search function to search within {total} more values…", "visualFeedback": { - "closestFeaturesAre": "{n} features within view", + "closestFeaturesAre": "{n} features within viewport.", "east": "Moving east", - "in": "Zooming in", + "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", "north": "Moving north", - "out": "Zooming out", + "oneFeatureInView": "One feature within viewport.", + "out": "Zooming out to level {z}", "south": "Moving south", "unlocked": "Moving enabled.", + "viewportCenterCloseToGps": "The map is centered around your location.", + "viewportCenterDetails": "The viewport center is {distance} away and {bearing} from your location.", "west": "Moving west" }, "waitingForGeopermission": "Waiting for your permission to use the geolocation…", diff --git a/langs/nl.json b/langs/nl.json index 14b2b20cd..420480c66 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -370,15 +370,15 @@ "useSearch": "Gebruik de zoekfunctie hierboven om meer opties te zien", "useSearchForMore": "Gebruik de zoekfunctie om {total} meer waarden te vinden…", "visualFeedback": { - "closestFeaturesAre": "{n} object in in beeld", + "closestFeaturesAre": "{n} object in beeld.", "east": "Naar het oosten", - "in": "Aan het inzoomen", + "in": "Aan het inzoomen naar zoomlevel {z}", "islocked": "Bewegen vergrendeld rond je huidige locatie. Duw op de geolocatie-knop om te ontgrendelen.", "locked": "Bewegen vergrendeld rond jouw huidige locatie.", "navigation": "Gebruik de pijltjestoetsen om te bewegen. Druk op spatie om het meest centrale punt te selecteren. Druk op een cijfertoets om andere items te selecteren.", "noCloseFeatures": "Niet in beeld", "north": "Naar het noorden", - "out": "Aan het uitzoomen", + "out": "Aan het uitzoomen naar zoomlevel {z}", "south": "Naar het zuiden", "unlocked": "Bewegen ontgrendeld", "west": "Naar het westen" diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index a81ff44a1..11a428aa6 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -1342,10 +1342,6 @@ video { resize: both; } -.list-none { - list-style-type: none; -} - .appearance-none { -webkit-appearance: none; appearance: none; @@ -1906,6 +1902,11 @@ video { line-height: 1; } +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + .text-3xl { font-size: 1.875rem; line-height: 2.25rem; @@ -1916,11 +1917,6 @@ video { line-height: 2rem; } -.text-sm { - font-size: 0.875rem; - line-height: 1.25rem; -} - .text-4xl { font-size: 2.25rem; line-height: 2.5rem; diff --git a/src/Logic/BBox.ts b/src/Logic/BBox.ts index 436145f4f..e69499c7a 100644 --- a/src/Logic/BBox.ts +++ b/src/Logic/BBox.ts @@ -288,4 +288,8 @@ export class BBox { throw "BBOX has NAN" } } + + public overlapsWithFeature(f: Feature) { + return GeoOperations.calculateOverlap(this.asGeoJson({}), [f]).length > 0 + } } diff --git a/src/Logic/FeatureSource/Sources/NearbyFeatureSource.ts b/src/Logic/FeatureSource/Sources/NearbyFeatureSource.ts index 703103bc0..4ca206498 100644 --- a/src/Logic/FeatureSource/Sources/NearbyFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/NearbyFeatureSource.ts @@ -4,28 +4,32 @@ import { Feature } from "geojson" import { GeoOperations } from "../../GeoOperations" import FilteringFeatureSource from "./FilteringFeatureSource" import LayerState from "../../State/LayerState" +import { BBox } from "../../BBox" export default class NearbyFeatureSource implements FeatureSource { - private readonly _result = new UIEventSource(undefined) - public readonly features: Store + private readonly _result = new UIEventSource(undefined) private readonly _targetPoint: Store<{ lon: number; lat: number }> private readonly _numberOfNeededFeatures: number private readonly _layerState?: LayerState private readonly _currentZoom: Store private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = [] - + private readonly _bounds: Store | undefined constructor( targetPoint: Store<{ lon: number; lat: number }>, sources: ReadonlyMap, - numberOfNeededFeatures?: number, - layerState?: LayerState, - currentZoom?: Store + options?: { + bounds?: Store + numberOfNeededFeatures?: number + layerState?: LayerState + currentZoom?: Store + } ) { - this._layerState = layerState + this._layerState = options?.layerState this._targetPoint = targetPoint.stabilized(100) - this._numberOfNeededFeatures = numberOfNeededFeatures - this._currentZoom = currentZoom.stabilized(500) + this._numberOfNeededFeatures = options?.numberOfNeededFeatures + this._currentZoom = options?.currentZoom.stabilized(500) + this._bounds = options?.bounds this.features = Stores.ListStabilized(this._result) @@ -53,6 +57,10 @@ export default class NearbyFeatureSource implements FeatureSource { private update() { let features: { feat: Feature; d: number }[] = [] for (const src of this._allSources) { + if (src.data === undefined) { + this._result.setData(undefined) + return // We cannot yet calculate all the features + } features.push(...src.data) } features.sort((a, b) => a.d - b.d) @@ -80,6 +88,15 @@ export default class NearbyFeatureSource implements FeatureSource { if (this._currentZoom.data < minZoom) { return empty } + if (this._bounds) { + const bbox = this._bounds.data + if (!bbox) { + // We have a 'bounds' store, but the bounds store itself is still empty + // As such, we cannot yet calculate which features are within the store + return undefined + } + feats = feats.filter((f) => bbox.overlapsWithFeature(f)) + } const point = this._targetPoint.data const lonLat = <[number, number]>[point.lon, point.lat] const withDistance = feats.map((feat) => ({ @@ -95,7 +112,7 @@ export default class NearbyFeatureSource implements FeatureSource { } return withDistance }, - [this._targetPoint, isActive, this._currentZoom] + [this._targetPoint, isActive, this._currentZoom, this._bounds] ) } } diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts index 61128d1b0..14ce8c85b 100644 --- a/src/Logic/GeoOperations.ts +++ b/src/Logic/GeoOperations.ts @@ -172,7 +172,7 @@ export class GeoOperations { } /** - * Detect wether or not the given point is located in the feature + * Detect whether or not the given point is located in the feature * * // Should work with a normal polygon * const polygon = {"type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [[[1.8017578124999998,50.401515322782366],[-3.1640625,46.255846818480315],[5.185546875,44.74673324024678],[1.8017578124999998,50.401515322782366]]]}}; @@ -985,4 +985,87 @@ export class GeoOperations { return result } + + /** + * GeoOperations.distanceToHuman(52.8) // => "53m" + * GeoOperations.distanceToHuman(2800) // => "2.8km" + * GeoOperations.distanceToHuman(12800) // => "13km" + * + * @param meters + */ + public static distanceToHuman(meters: number): string { + meters = Math.round(meters) + if (meters < 1000) { + return meters + "m" + } + + if (meters >= 10000) { + const km = Math.round(meters / 1000) + return km + "km" + } + + meters = Math.round(meters / 100) + const kmStr = "" + meters + + return kmStr.substring(0, kmStr.length - 1) + "." + kmStr.substring(kmStr.length - 1) + "km" + } + + private static readonly directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const + private static readonly directionsRelative = [ + "straight", + "slight_right", + "right", + "sharp_right", + "behind", + "sharp_left", + "left", + "slight_left", + ] as const + + /** + * GeoOperations.bearingToHuman(0) // => "N" + * GeoOperations.bearingToHuman(-9) // => "N" + * GeoOperations.bearingToHuman(-10) // => "N" + * GeoOperations.bearingToHuman(-180) // => "S" + * GeoOperations.bearingToHuman(181) // => "S" + * GeoOperations.bearingToHuman(46) // => "NE" + */ + public static bearingToHuman( + bearing: number + ): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" { + while (bearing < 0) { + bearing += 360 + } + bearing %= 360 + bearing += 22.5 + const segment = Math.floor(bearing / 45) % GeoOperations.directions.length + return GeoOperations.directions[segment] + } + + /** + * GeoOperations.bearingToHuman(0) // => "N" + * GeoOperations.bearingToHuman(-10) // => "N" + * GeoOperations.bearingToHuman(-180) // => "S" + * GeoOperations.bearingToHuman(181) // => "S" + * GeoOperations.bearingToHuman(46) // => "NE" + */ + public static bearingToHumanRelative( + bearing: number + ): + | "straight" + | "slight_right" + | "right" + | "sharp_right" + | "behind" + | "sharp_left" + | "left" + | "slight_left" { + while (bearing < 0) { + bearing += 360 + } + bearing %= 360 + bearing += 22.5 + const segment = Math.floor(bearing / 45) % GeoOperations.directionsRelative.length + return GeoOperations.directionsRelative[segment] + } } diff --git a/src/Logic/State/UserSettingsMetaTagging.ts b/src/Logic/State/UserSettingsMetaTagging.ts index 6e568c5c3..33a5ae85b 100644 --- a/src/Logic/State/UserSettingsMetaTagging.ts +++ b/src/Logic/State/UserSettingsMetaTagging.ts @@ -1,42 +1,14 @@ import { Utils } from "../../Utils" /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ export class ThemeMetaTagging { - public static readonly themeName = "usersettings" + public static readonly themeName = "usersettings" - public metaTaggging_for_usersettings(feat: { properties: Record }) { - Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => - feat.properties._description - .match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) - ?.at(1) - ) - Utils.AddLazyProperty( - feat.properties, - "_d", - () => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? "" - ) - Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () => - ((feat) => { - const e = document.createElement("div") - e.innerHTML = feat.properties._d - return Array.from(e.getElementsByTagName("a")).filter( - (a) => a.href.match(/mastodon|en.osm.town/) !== null - )[0]?.href - })(feat) - ) - Utils.AddLazyProperty(feat.properties, "_mastodon_link", () => - ((feat) => { - const e = document.createElement("div") - e.innerHTML = feat.properties._d - return Array.from(e.getElementsByTagName("a")).filter( - (a) => a.getAttribute("rel")?.indexOf("me") >= 0 - )[0]?.href - })(feat) - ) - Utils.AddLazyProperty( - feat.properties, - "_mastodon_candidate", - () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a - ) - feat.properties["__current_backgroun"] = "initial_value" - } -} + public metaTaggging_for_usersettings(feat: {properties: Record}) { + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) ) + Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' ) + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) ) + Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) ) + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a ) + feat.properties['__current_backgroun'] = 'initial_value' + } +} \ No newline at end of file diff --git a/src/Logic/UIEventSource.ts b/src/Logic/UIEventSource.ts index f1321170e..ed48130ff 100644 --- a/src/Logic/UIEventSource.ts +++ b/src/Logic/UIEventSource.ts @@ -10,6 +10,9 @@ export class Stores { function run() { source.setData(new Date()) + if (Utils.runningFromConsole) { + return + } if (asLong === undefined || asLong()) { window.setTimeout(run, millis) } @@ -104,7 +107,8 @@ export abstract class Store implements Readable { M public mapD( f: (t: Exclude) => J, - extraStoresToWatch?: Store[] + extraStoresToWatch?: Store[], + callbackDestroyFunction?: (f: () => void) => void ): Store { return this.map((t) => { if (t === undefined) { @@ -263,7 +267,7 @@ export abstract class Store implements Readable { /** * Converts the uiEventSource into a promise. * The promise will return the value of the store if the given condition evaluates to true - * @param condition: an optional condition, default to 'store.value !== undefined' + * @param condition an optional condition, default to 'store.value !== undefined' * @constructor */ public AsPromise(condition?: (t: T) => boolean): Promise { @@ -482,7 +486,7 @@ class MappedStore extends Store { stores = [] } if (extraStores?.length > 0) { - stores.push(...extraStores) + stores?.push(...extraStores) } if (this._extraStores?.length > 0) { this._extraStores?.forEach((store) => { @@ -767,9 +771,9 @@ export class UIEventSource extends Store implements Writable { /** * Monoidal map which results in a read-only store * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' - * @param f: The transforming function - * @param extraSources: also trigger the update if one of these sources change - * @param onDestroy: a callback that can trigger the destroy function + * @param f The transforming function + * @param extraSources also trigger the update if one of these sources change + * @param onDestroy a callback that can trigger the destroy function * * const src = new UIEventSource(10) * const store = src.map(i => i * 2) @@ -802,7 +806,8 @@ export class UIEventSource extends Store implements Writable { */ public mapD( f: (t: Exclude) => J, - extraSources: Store[] = [] + extraSources: Store[] = [], + callbackDestroyFunction?: (f: () => void) => void ): Store { return new MappedStore( this, @@ -819,17 +824,18 @@ export class UIEventSource extends Store implements Writable { this._callbacks, this.data === undefined || this.data === null ? this.data - : f(this.data) + : f(this.data), + callbackDestroyFunction ) } /** * Two way sync with functions in both directions * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' - * @param f: The transforming function - * @param extraSources: also trigger the update if one of these sources change - * @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData - * @param allowUnregister: if set, the update will be halted if no listeners are registered + * @param f The transforming function + * @param extraSources also trigger the update if one of these sources change + * @param g a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData + * @param allowUnregister if set, the update will be halted if no listeners are registered */ public sync( f: (t: T) => J, diff --git a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts index 241d24c67..4c6211cc7 100644 --- a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts +++ b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts @@ -105,6 +105,12 @@ export interface MappingConfigJson { */ hideInAnswer?: boolean | TagConfigJson + /** + * Also show this 'then'-option if the feature matches these tags. + * Ideal for outdated tags. + */ + alsoShowIf?: TagConfigJson + /** * question: What tags should be applied if this mapping is _not_ chosen? * diff --git a/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts b/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts index 31f9c3ce8..450634f71 100644 --- a/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts +++ b/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts @@ -168,6 +168,7 @@ export interface TagRenderingConfigJson { * This can be an substituting-tag as well, e.g. {'if': 'addr:street:={_calculated_nearby_streetname}', 'then': '{_calculated_nearby_streetname}'} */ if: TagConfigJson + /** * question: What text should be shown? * diff --git a/src/Models/ThemeConfig/TagRenderingConfig.ts b/src/Models/ThemeConfig/TagRenderingConfig.ts index a7c445706..b8f09527c 100644 --- a/src/Models/ThemeConfig/TagRenderingConfig.ts +++ b/src/Models/ThemeConfig/TagRenderingConfig.ts @@ -25,6 +25,7 @@ export interface Icon {} export interface Mapping { readonly if: UploadableTag + readonly alsoShowIf: Tag | undefined readonly ifnot?: UploadableTag readonly then: TypedTranslation readonly icon: string @@ -383,7 +384,9 @@ export default class TagRenderingConfig { } } const prioritySearch = - mapping.priorityIf !== undefined ? TagUtils.Tag(mapping.priorityIf) : undefined + mapping.priorityIf !== undefined + ? TagUtils.Tag(mapping.priorityIf, `${ctx}.priorityIf`) + : undefined const mp = { if: TagUtils.Tag(mapping.if, `${ctx}.if`), ifnot: @@ -391,6 +394,10 @@ export default class TagRenderingConfig { ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) : undefined, then: Translations.T(mapping.then, `${ctx}.then`), + alsoShowIf: + mapping.alsoShowIf !== undefined + ? TagUtils.Tag(mapping.alsoShowIf, `${ctx}.alsoShowIf`) + : undefined, hideInAnswer, icon, iconClass, @@ -530,6 +537,9 @@ export default class TagRenderingConfig { if (mapping.if.matchesProperties(tags)) { return mapping } + if (mapping.alsoShowIf?.matchesProperties(tags)) { + return mapping + } } } @@ -818,6 +828,7 @@ export default class TagRenderingConfig { for (const m of this.mappings ?? []) { tags.push(m.if) tags.push(m.priorityIf) + tags.push(m.alsoShowIf) tags.push(...(m.addExtraTags ?? [])) if (typeof m.hideInAnswer !== "boolean") { tags.push(m.hideInAnswer) diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 96852cdba..4f2c2ad66 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -119,6 +119,11 @@ export default class ThemeViewState implements SpecialVisualizationState { readonly previewedImage = new UIEventSource(undefined) readonly addNewPoint: UIEventSource = new UIEventSource(false) + /** + * When using arrow keys to move, the accessibility mode is activated, which has a small rectangle set. + * This is the 'viewport' which 'closestFeatures' uses to filter wilt + */ + readonly visualFeedbackViewportBounds: UIEventSource = new UIEventSource(undefined) readonly lastClickObject: LastClickFeatureSource readonly overlayLayerStates: ReadonlyMap< @@ -351,9 +356,11 @@ export default class ThemeViewState implements SpecialVisualizationState { this.closestFeatures = new NearbyFeatureSource( this.mapProperties.location, this.perLayerFiltered, - 3, - this.layerState, - this.mapProperties.zoom + { + currentZoom: this.mapProperties.zoom, + layerState: this.layerState, + bounds: this.visualFeedbackViewportBounds, + } ) this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView this.imageUploadManager = new ImageUploadManager( @@ -476,8 +483,18 @@ export default class ThemeViewState implements SpecialVisualizationState { */ private selectClosestAtCenter(i: number = 0) { this.visualFeedback.setData(true) - const toSelect = this.closestFeatures.features.data[i] + const toSelect = this.closestFeatures.features?.data?.[i] if (!toSelect) { + window.requestAnimationFrame(() => { + const toSelect = this.closestFeatures.features?.data?.[i] + if (!toSelect) { + return + } + const layer = this.layout.getMatchingLayer(toSelect.properties) + this.selectedElement.setData(undefined) + this.selectedLayer.setData(layer) + this.selectedElement.setData(toSelect) + }) return } const layer = this.layout.getMatchingLayer(toSelect.properties) diff --git a/src/UI/Base/DirectionIndicator.svelte b/src/UI/Base/DirectionIndicator.svelte new file mode 100644 index 000000000..6c2741503 --- /dev/null +++ b/src/UI/Base/DirectionIndicator.svelte @@ -0,0 +1,47 @@ + + +
+
+ {GeoOperations.distanceToHuman($bearingAndDist.dist)} +
+ {#if $bearingFromGps !== undefined} +
+ +
+ {/if} +
+{$bearingAndDist.bearing}° {GeoOperations.bearingToHuman($bearingAndDist.bearing)} {GeoOperations.bearingToHumanRelative($bearingAndDist.bearing - $compass)} diff --git a/src/UI/Base/Hotkeys.ts b/src/UI/Base/Hotkeys.ts index 1359163b5..ec4068e1b 100644 --- a/src/UI/Base/Hotkeys.ts +++ b/src/UI/Base/Hotkeys.ts @@ -47,7 +47,8 @@ export default class Hotkeys { onUp?: boolean }, documentation: string | Translation, - action: () => void | false + action: () => void | false, + alsoTriggeredOn?: Translation[] ) { const type = key["onUp"] ? "keyup" : "keypress" let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] diff --git a/src/UI/Base/Link.svelte b/src/UI/Base/Link.svelte index dd838e41c..f7bae099f 100644 --- a/src/UI/Base/Link.svelte +++ b/src/UI/Base/Link.svelte @@ -11,6 +11,7 @@ + import { Store } from "../../Logic/UIEventSource" + import { GeoOperations } from "../../Logic/GeoOperations" + import ThemeViewState from "../../Models/ThemeViewState" + import Tr from "../Base/Tr.svelte" + import Translations from "../i18n/Translations" + + /** + * Indicates how far away the viewport center is from the current user location + */ + export let state: ThemeViewState + const t = Translations.t.general.visualFeedback + let map = state.mapProperties + + let currentLocation = state.geolocation.geolocationState.currentGPSLocation + let distanceToCurrentLocation: Store<{ distance: string, distanceInMeters: number, bearing: number }> = map.location.mapD(({ lon, lat }) => { + const current = currentLocation.data + if (!current) { + return undefined + } + const gps: [number, number] = [current.longitude, current.latitude] + const mapCenter: [number, number] = [lon, lat] + 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 } + }, [currentLocation]) + + +{#if $currentLocation !== undefined} + {#if $distanceToCurrentLocation.distanceInMeters < 20} + + {:else} + + {/if} +{/if} diff --git a/src/UI/BigComponents/ReverseGeocoding.svelte b/src/UI/BigComponents/ReverseGeocoding.svelte index d0979dda0..98a902c65 100644 --- a/src/UI/BigComponents/ReverseGeocoding.svelte +++ b/src/UI/BigComponents/ReverseGeocoding.svelte @@ -8,8 +8,11 @@ import Hotkeys from "../Base/Hotkeys" import Translations from "../i18n/Translations" import Locale from "../i18n/Locale" + import MapCenterDetails from "./MapCenterDetails.svelte" + import ThemeViewState from "../../Models/ThemeViewState" - export let mapProperties: MapProperties + export let state: ThemeViewState + let mapProperties = state.mapProperties let lastDisplayed: Date = undefined let currentLocation: string = undefined @@ -51,8 +54,9 @@ {/if} diff --git a/src/UI/BigComponents/SelectedElementTitle.svelte b/src/UI/BigComponents/SelectedElementTitle.svelte index aa6ea5a78..65da3dc42 100644 --- a/src/UI/BigComponents/SelectedElementTitle.svelte +++ b/src/UI/BigComponents/SelectedElementTitle.svelte @@ -31,8 +31,11 @@

+ +

+