forked from MapComplete/MapComplete
		
	A11y: various improvements
This commit is contained in:
		
							parent
							
								
									0d4f2c9c36
								
							
						
					
					
						commit
						5fa2ddd9c1
					
				
					 23 changed files with 327 additions and 98 deletions
				
			
		|  | @ -189,6 +189,10 @@ | |||
|         "addExtraTags": [ | ||||
|           "contact:phone=" | ||||
|         ] | ||||
|       }, | ||||
|       "editButtonAriaLabel": { | ||||
|         "en": "Edit phone number", | ||||
|         "nl": "Pas telefoonnummer aan" | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|  | @ -268,6 +272,10 @@ | |||
|         "addExtraTags": [ | ||||
|           "contact:email=" | ||||
|         ] | ||||
|       }, | ||||
|       "editButtonAriaLabel": { | ||||
|         "en": "Edit email address", | ||||
|         "nl": "Pas emailadres aan" | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|  | @ -318,7 +326,11 @@ | |||
|           "hideInAnswer": true, | ||||
|           "icon": "./assets/layers/icons/website.svg" | ||||
|         } | ||||
|       ] | ||||
|       ], | ||||
|       "editButtonAriaLabel": { | ||||
|         "en": "Edit website", | ||||
|         "nl": "Pas website aan" | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "id": "wheelchair-access", | ||||
|  |  | |||
|  | @ -306,7 +306,7 @@ | |||
|             "sunday": "On sunday {ranges}", | ||||
|             "thursday": "On thursday {ranges}", | ||||
|             "tuesday": "On tuesday {ranges}", | ||||
|             "unknown": "The opening hours are unkown", | ||||
|             "unknown": "The opening hours are unknown", | ||||
|             "wednesday": "On wednesday {ranges}" | ||||
|         }, | ||||
|         "osmLinkTooltip": "Browse this object on OpenStreetMap for history and more editing options", | ||||
|  | @ -398,12 +398,12 @@ | |||
|         "useSearch": "Use the search above to see presets", | ||||
|         "useSearchForMore": "Use the search function to search within {total} more values…", | ||||
|         "visualFeedback": { | ||||
|             "closestFeaturesAre": "Closest features are:", | ||||
|             "closestFeaturesAre": "{n} features within view", | ||||
|             "east": "Moving east", | ||||
|             "in": "Zooming in", | ||||
|             "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", | ||||
|             "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", | ||||
|  |  | |||
|  | @ -50,6 +50,12 @@ | |||
|         "panelIntro": "<h3>Jouw persoonlijke thema</h3>Activeer je favorite lagen van alle andere themas", | ||||
|         "reload": "Herlaad de data" | ||||
|     }, | ||||
|     "favouritePoi": { | ||||
|         "button": { | ||||
|             "isMarkedShort": "Als favoriet gemarkeerd", | ||||
|             "isNotMarkedShort": "Niet als favoriet gemarkeerd" | ||||
|         } | ||||
|     }, | ||||
|     "flyer": { | ||||
|         "aerial": "Deze kaart gebruikt luchtfoto's van het Agentschap Informatie Vlaanderen als achtergrond.\nOok het GRB is beschikbaar als achtergrondlaag.", | ||||
|         "callToAction": "Probeer het uit op mapcomplete.org", | ||||
|  | @ -162,6 +168,7 @@ | |||
|         }, | ||||
|         "back": "Vorige", | ||||
|         "backToIndex": "Keer terug naar het overzicht met alle thematische kaarten", | ||||
|         "backToMap": "Ga terug naar de kaart", | ||||
|         "backgroundMap": "Selecteer een achtergrondlaag", | ||||
|         "backgroundSwitch": "Verander achtergrond", | ||||
|         "cancel": "Annuleren", | ||||
|  | @ -200,6 +207,14 @@ | |||
|         "histogram": { | ||||
|             "error_loading": "Kan het histogram niet laden" | ||||
|         }, | ||||
|         "labels": { | ||||
|             "background": "Kies achtergrondlaag", | ||||
|             "filter": "Filter data", | ||||
|             "jumpToLocation": "Ga naar jouw locatie", | ||||
|             "menu": "Menu", | ||||
|             "zoomIn": "Zoom in", | ||||
|             "zoomOut": "Zoom uit" | ||||
|         }, | ||||
|         "layerSelection": { | ||||
|             "title": "Selecteer lagen", | ||||
|             "zoomInToSeeThisLayer": "Vergroot de kaart om deze laag te zien" | ||||
|  | @ -245,11 +260,17 @@ | |||
|         "openTheMap": "Raadpleeg de kaart", | ||||
|         "openTheMapAtGeolocation": "Ga naar jouw locatie", | ||||
|         "opening_hours": { | ||||
|             "all_days_from": "Elke dag geopend {ranges}", | ||||
|             "closed_permanently": "Gesloten voor onbepaalde tijd", | ||||
|             "closed_until": "Gesloten - open op {date}", | ||||
|             "error": "Kan de openingsuren niet inlezen", | ||||
|             "error_loading": "Sorry, deze openingsuren kunnen niet getoond worden", | ||||
|             "friday": "Op vrijdag {ranges}", | ||||
|             "loadingCountry": "Het land wordt nog bepaald…", | ||||
|             "monday": "Op maandag {ranges}", | ||||
|             "not_all_rules_parsed": "De openingsuren zijn ingewikkeld. De volgende regels worden niet getoond bij het ingeven:", | ||||
|             "on_weekdays": "Op weekdagen {ranges}", | ||||
|             "on_weekends": "In het weekend {ranges}", | ||||
|             "openTill": "tot", | ||||
|             "open_24_7": "Dag en nacht open", | ||||
|             "open_during_ph": "Op een feestdag is dit", | ||||
|  | @ -257,7 +278,15 @@ | |||
|             "ph_closed": "gesloten", | ||||
|             "ph_not_known": " ", | ||||
|             "ph_open": "open", | ||||
|             "ph_open_as_usual": "geopend zoals gewoonlijk" | ||||
|             "ph_open_as_usual": "geopend zoals gewoonlijk", | ||||
|             "ranges": "van {starttime} tot {endtime}", | ||||
|             "rangescombined": "{range0} en {range1}", | ||||
|             "saturday": "Op zaterdag {ranges}", | ||||
|             "sunday": "Op zondag {ranges}", | ||||
|             "thursday": "Op donderdag {ranges}", | ||||
|             "tuesday": "Op dinsdag {ranges}", | ||||
|             "unknown": "De openingsuren zijn niet gekend", | ||||
|             "wednesday": "Op woensdag {ranges}" | ||||
|         }, | ||||
|         "osmLinkTooltip": "Bekijk dit object op OpenStreetMap om de geschiedenis te zien en meer te kunnen aanpassen", | ||||
|         "pdf": { | ||||
|  | @ -300,6 +329,7 @@ | |||
|             "searchShort": "Zoek…", | ||||
|             "searching": "Aan het zoeken…" | ||||
|         }, | ||||
|         "share": "Deel deze locatie", | ||||
|         "sharescreen": { | ||||
|             "copiedToClipboard": "Link gekopieerd naar klembord", | ||||
|             "embedIntro": "<h3>Plaats dit op je website</h3>Voeg dit kaartje toe op je eigen website.<br/>We moedigen dit zelfs aan - je hoeft geen toestemming te vragen.<br/> Het is gratis en zal dat altijd blijven. Hoe meer het gebruikt wordt, hoe waardevoller", | ||||
|  | @ -340,6 +370,20 @@ | |||
|         }, | ||||
|         "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", | ||||
|             "east": "Naar het oosten", | ||||
|             "in": "Aan het inzoomen", | ||||
|             "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", | ||||
|             "south": "Naar het zuiden", | ||||
|             "unlocked": "Bewegen ontgrendeld", | ||||
|             "west": "Naar het westen" | ||||
|         }, | ||||
|         "weekdays": { | ||||
|             "abbreviations": { | ||||
|                 "friday": "Vrij", | ||||
|  |  | |||
|  | @ -32,7 +32,7 @@ | |||
|       "https://overpass.openstreetmap.ru/cgi/interpreter" | ||||
|     ], | ||||
|     "country_coder_host": "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country", | ||||
|     "nominatimEndpoint": "https://nominatim.openstreetmap.org/search?" | ||||
|     "nominatimEndpoint": "https://nominatim.openstreetmap.org/" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "start": "npm run generate:layeroverview && npm run strt", | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { Utils } from "../../Utils" | ||||
| import { BBox } from "../BBox" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { FeatureCollection } from "geojson" | ||||
| 
 | ||||
| export interface GeoCodeResult { | ||||
|     display_name: string | ||||
|  | @ -20,12 +21,21 @@ export class Geocoding { | |||
| 
 | ||||
|     static async Search(query: string, bbox: BBox): Promise<GeoCodeResult[]> { | ||||
|         const b = bbox ?? BBox.global | ||||
|         const url = | ||||
|             Geocoding.host + | ||||
|             "format=json&limit=1&viewbox=" + | ||||
|             `${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` + | ||||
|             "&accept-language=nl&q=" + | ||||
|             query | ||||
|         const url = `${ | ||||
|             Geocoding.host | ||||
|         }search?format=json&limit=1&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=nl&q=${query}` | ||||
|         return Utils.downloadJson(url) | ||||
|     } | ||||
| 
 | ||||
|     static async reverse( | ||||
|         coordinate: { lon: number; lat: number }, | ||||
|         zoom: number = 18 | ||||
|     ): Promise<FeatureCollection> { | ||||
|         // https://nominatim.org/release-docs/develop/api/Reverse/
 | ||||
|         // IF the zoom is low, it'll only return a country instead of an address
 | ||||
|         const url = `${Geocoding.host}reverse?format=geojson&lat=${coordinate.lat}&lon=${ | ||||
|             coordinate.lon | ||||
|         }&zoom=${Math.round(zoom)}` | ||||
|         return Utils.downloadJson(url) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ export interface MapProperties { | |||
|     readonly maxbounds: UIEventSource<undefined | BBox> | ||||
|     readonly allowMoving: UIEventSource<true | boolean> | ||||
|     readonly allowRotating: UIEventSource<true | boolean> | ||||
|     readonly rotation: UIEventSource<number> | ||||
|     readonly lastClickLocation: Store<{ lon: number; lat: number }> | ||||
|     readonly allowZooming: UIEventSource<true | boolean> | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,13 +14,7 @@ export type MenuViewTabStates = (typeof MenuState._menuviewTabs)[number] | |||
|  * Some convenience methods are provided for this as well | ||||
|  */ | ||||
| export class MenuState { | ||||
|     public static readonly _themeviewTabs = [ | ||||
|         "intro", | ||||
|         "filters", | ||||
|         "download", | ||||
|         "copyright", | ||||
|         "share", | ||||
|     ] as const | ||||
|     public static readonly _themeviewTabs = ["intro", "download", "copyright", "share"] as const | ||||
|     public static readonly _menuviewTabs = [ | ||||
|         "about", | ||||
|         "settings", | ||||
|  | @ -39,6 +33,8 @@ export class MenuState { | |||
|     public readonly backgroundLayerSelectionIsOpened: UIEventSource<boolean> = | ||||
|         new UIEventSource<boolean>(false) | ||||
| 
 | ||||
|     public readonly filtersPanelIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
| 
 | ||||
|     public readonly allToggles: { | ||||
|         toggle: UIEventSource<boolean> | ||||
|         name: string | ||||
|  | @ -84,8 +80,8 @@ export class MenuState { | |||
|                 this.highlightedUserSetting.setData(undefined) | ||||
|             } | ||||
|         }) | ||||
|         this.themeViewTab.addCallbackAndRun((tab) => { | ||||
|             if (tab !== "filters") { | ||||
|         this.filtersPanelIsOpened.addCallbackAndRun((isOpen) => { | ||||
|             if (!isOpen) { | ||||
|                 this.highlightedLayerInFilters.setData(undefined) | ||||
|             } | ||||
|         }) | ||||
|  | @ -121,8 +117,7 @@ export class MenuState { | |||
|     } | ||||
| 
 | ||||
|     public openFilterView(highlightLayer?: LayerConfig | string) { | ||||
|         this.themeIsOpened.setData(true) | ||||
|         this.themeViewTab.setData("filters") | ||||
|         this.filtersPanelIsOpened.setData(true) | ||||
|         if (highlightLayer) { | ||||
|             if (typeof highlightLayer !== "string") { | ||||
|                 highlightLayer = highlightLayer.id | ||||
|  | @ -159,6 +154,7 @@ export class MenuState { | |||
|             this.menuIsOpened, | ||||
|             this.themeIsOpened, | ||||
|             this.backgroundLayerSelectionIsOpened, | ||||
|             this.filtersPanelIsOpened, | ||||
|         ] | ||||
|         const somethingIsOpen = toggles.some((t) => t.data) | ||||
|         toggles.forEach((t) => t.setData(false)) | ||||
|  |  | |||
|  | @ -279,6 +279,13 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs | |||
|      */ | ||||
|     questionHint?: Translatable | ||||
| 
 | ||||
|     /** | ||||
|      * When using a screenreader and selecting the 'edit' button, the current rendered value is read aloud in normal circumstances. | ||||
|      * In some rare cases, this is not desirable. For example, if the rendered value is a link to a website, this link can be selected (and will be read aloud). | ||||
|      * If the user presses _tab_ again, they'll select the button and have the link read aloud a second time. | ||||
|      */ | ||||
|     editButtonAriaLabel?: Translatable | ||||
| 
 | ||||
|     /** | ||||
|      * A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer | ||||
|      */ | ||||
|  |  | |||
|  | @ -76,6 +76,7 @@ export default class TagRenderingConfig { | |||
|     public readonly multiAnswer: boolean | ||||
| 
 | ||||
|     public readonly mappings?: Mapping[] | ||||
|     public readonly editButtonAriaLabel?: Translation | ||||
|     public readonly labels: string[] | ||||
|     public readonly classes: string[] | ||||
| 
 | ||||
|  | @ -134,6 +135,11 @@ export default class TagRenderingConfig { | |||
|         this.question = Translations.T(json.question, translationKey + ".question") | ||||
|         this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint") | ||||
|         this.description = Translations.T(json.description, translationKey + ".description") | ||||
|         this.editButtonAriaLabel = Translations.T( | ||||
|             json.editButtonAriaLabel, | ||||
|             translationKey + ".editButtonAriaLabel" | ||||
|         ) | ||||
| 
 | ||||
|         this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`) | ||||
|         this.invalidValues = json["invalidValues"] | ||||
|             ? TagUtils.Tag(json["invalidValues"], `${context}.invalidValues`) | ||||
|  |  | |||
|  | @ -61,6 +61,7 @@ import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSou | |||
| import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource" | ||||
| import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider" | ||||
| import { GeolocationControlState } from "../UI/BigComponents/GeolocationControl" | ||||
| import { Orientation } from "../Sensors/Orientation" | ||||
| 
 | ||||
| /** | ||||
|  * | ||||
|  | @ -115,8 +116,6 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|     readonly geolocation: GeoLocationHandler | ||||
|     readonly geolocationControl: GeolocationControlState | ||||
| 
 | ||||
|     readonly lastGeolocationRequestMoment: UIEventSource<Date> = new UIEventSource<Date>(undefined) | ||||
| 
 | ||||
|     readonly imageUploadManager: ImageUploadManager | ||||
|     readonly previewedImage = new UIEventSource<ProvidedImage>(undefined) | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										33
									
								
								src/Sensors/Motion.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/Sensors/Motion.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| import { UIEventSource } from "../Logic/UIEventSource" | ||||
| 
 | ||||
| export default class Motion { | ||||
|     public static singleton = new Motion() | ||||
|     /** | ||||
|      * In m/s² | ||||
|      */ | ||||
|     public maxAcc = new UIEventSource<number>(0) | ||||
| 
 | ||||
|     public lastShakeEvent = new UIEventSource<Date>(undefined) | ||||
| 
 | ||||
|     private isListening = false | ||||
|     private constructor() { | ||||
|         this.startListening() | ||||
|     } | ||||
| 
 | ||||
|     private onUpdate(eventData: DeviceMotionEvent) { | ||||
|         const acc = eventData.acceleration | ||||
|         this.maxAcc.setData(Math.max(acc.x, acc.y, acc.z)) | ||||
|         if (this.maxAcc.data > 22) { | ||||
|             this.lastShakeEvent.setData(new Date()) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     startListening() { | ||||
|         if (this.isListening) { | ||||
|             return | ||||
|         } | ||||
|         this.isListening = true | ||||
|         console.log("Listening to motion events", this) | ||||
|         window.addEventListener("devicemotion", (e) => this.onUpdate(e)) | ||||
|     } | ||||
| } | ||||
|  | @ -30,9 +30,13 @@ export class Orientation { | |||
|      */ | ||||
|     public arrowDirection: UIEventSource<number> = new UIEventSource(undefined) | ||||
|     private _measurementsStarted = false | ||||
|     private _animateFakeMeasurements = false | ||||
| 
 | ||||
|     constructor() {} | ||||
|     constructor() { | ||||
|         // this.fakeMeasurements(true)
 | ||||
|     } | ||||
| 
 | ||||
|     // noinspection JSUnusedGlobalSymbols
 | ||||
|     public fakeMeasurements(rotateAlpha: boolean = true) { | ||||
|         console.log("Starting fake measurements of orientation sensors", { | ||||
|             alpha: this.alpha, | ||||
|  | @ -41,10 +45,15 @@ export class Orientation { | |||
|             absolute: this.absolute, | ||||
|         }) | ||||
|         this.alpha.setData(45) | ||||
| 
 | ||||
|         if (rotateAlpha) { | ||||
|             Stores.Chronic(25).addCallback((date) => | ||||
|                 this.alpha.setData(-(date.getTime() / 10) % 360) | ||||
|             ) | ||||
|             this._animateFakeMeasurements = true | ||||
|             Stores.Chronic(25).addCallback((date) => { | ||||
|                 this.alpha.setData((date.getTime() / 100) % 360) | ||||
|                 if (!this._animateFakeMeasurements) { | ||||
|                     return true | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|         this.beta.setData(20) | ||||
|         this.gamma.setData(30) | ||||
|  |  | |||
							
								
								
									
										39
									
								
								src/UI/BigComponents/FilterPanel.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/UI/BigComponents/FilterPanel.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| <script lang="ts"> | ||||
|   /** | ||||
|    * THe panel containing all filter- and layerselection options | ||||
|    */ | ||||
| 
 | ||||
|   import OverlayToggle from "./OverlayToggle.svelte" | ||||
|   import Filterview from "./Filterview.svelte" | ||||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
|   import Filter from "../../assets/svg/Filter.svelte" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
|   let layout = state.layout | ||||
| </script> | ||||
| 
 | ||||
| <div class="m-2 flex flex-col"> | ||||
| 
 | ||||
|   <h2 class="flex items-center"> | ||||
|     <Filter class="h-6 w-6 pr-2" /> | ||||
|     <Tr t={Translations.t.general.menu.filter} /> | ||||
|   </h2> | ||||
| 
 | ||||
|   {#each layout.layers as layer} | ||||
|     <Filterview | ||||
|       zoomlevel={state.mapProperties.zoom} | ||||
|       filteredLayer={state.layerState.filteredLayers.get(layer.id)} | ||||
|       highlightedLayer={state.guistate.highlightedLayerInFilters} | ||||
|     /> | ||||
|   {/each} | ||||
|   {#each layout.tileLayerSources as tilesource} | ||||
|     <OverlayToggle | ||||
|       layerproperties={tilesource} | ||||
|       state={state.overlayLayerStates.get(tilesource.id)} | ||||
|       highlightedLayer={state.guistate.highlightedLayerInFilters} | ||||
|       zoomlevel={state.mapProperties.zoom} | ||||
|     /> | ||||
|   {/each} | ||||
| </div> | ||||
|  | @ -1,8 +1,6 @@ | |||
| <script lang="ts"> | ||||
|   import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { Feature } from "geojson" | ||||
|   import ToSvelte from "../Base/ToSvelte.svelte" | ||||
|   import Svg from "../../Svg.js" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
|   import Hotkeys from "../Base/Hotkeys" | ||||
|  | @ -23,7 +21,7 @@ | |||
|   onDestroy( | ||||
|     triggerSearch.addCallback((_) => { | ||||
|       performSearch() | ||||
|     }) | ||||
|     }), | ||||
|   ) | ||||
| 
 | ||||
|   let isRunning: boolean = false | ||||
|  | @ -32,14 +30,16 @@ | |||
| 
 | ||||
|   let feedback: string = undefined | ||||
| 
 | ||||
|    | ||||
|   function focusOnSearch() { | ||||
|     requestAnimationFrame(() => { | ||||
|       inputElement?.focus() | ||||
|       inputElement?.select() | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => { | ||||
|     feedback = undefined | ||||
|     requestAnimationFrame(() => { | ||||
|     inputElement?.focus() | ||||
|     inputElement?.select() | ||||
|     }) | ||||
|     focusOnSearch() | ||||
|   }) | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ searchCompleted; searchIsValid: boolean }>() | ||||
|  | @ -62,6 +62,7 @@ | |||
|       const result = await Geocoding.Search(searchContents, bounds.data) | ||||
|       if (result.length == 0) { | ||||
|         feedback = Translations.t.general.search.nothing.txt | ||||
|         focusOnSearch() | ||||
|         return | ||||
|       } | ||||
|       const poi = result[0] | ||||
|  | @ -70,7 +71,7 @@ | |||
|         new BBox([ | ||||
|           [lon0, lat0], | ||||
|           [lon1, lat1], | ||||
|         ]).pad(0.01) | ||||
|         ]).pad(0.01), | ||||
|       ) | ||||
|       if (perLayer !== undefined) { | ||||
|         const id = poi.osm_type + "/" + poi.osm_id | ||||
|  | @ -78,11 +79,11 @@ | |||
|         for (const layer of layers) { | ||||
|           const found = layer.features.data.find((f) => f.properties.id === id) | ||||
|           if (found === undefined) { | ||||
|             continue; | ||||
|             continue | ||||
|           } | ||||
|           selectedElement?.setData(found); | ||||
|           console.log("Found an element that probably matches:", selectedElement?.data); | ||||
|           break; | ||||
|           selectedElement?.setData(found) | ||||
|           console.log("Found an element that probably matches:", selectedElement?.data) | ||||
|           break | ||||
|         } | ||||
|       } | ||||
|       if (clearAfterView) { | ||||
|  | @ -93,6 +94,7 @@ | |||
|     } catch (e) { | ||||
|       console.error(e) | ||||
|       feedback = Translations.t.general.search.error.txt | ||||
|       focusOnSearch() | ||||
|     } finally { | ||||
|       isRunning = false | ||||
|     } | ||||
|  | @ -100,23 +102,25 @@ | |||
| </script> | ||||
| 
 | ||||
| <div class="normal-background flex justify-between rounded-full pl-2"> | ||||
|   <form class="w-full"> | ||||
|   <form class="w-full flex flex-wrap"> | ||||
|     {#if isRunning} | ||||
|       <Loading>{Translations.t.general.search.searching}</Loading> | ||||
|     {:else if feedback !== undefined} | ||||
|       <div class="alert" on:click={() => (feedback = undefined)}> | ||||
|         {feedback} | ||||
|       </div> | ||||
|     {:else} | ||||
|       <input | ||||
|         type="search" | ||||
|         class="w-full" | ||||
|         bind:this={inputElement} | ||||
|         on:keypress={(keypr) => (keypr.key === "Enter" ? performSearch() : undefined)} | ||||
|         on:keypress={(keypr) =>{  feedback = undefined; return (keypr.key === "Enter" ? performSearch() : undefined); }} | ||||
|         bind:value={searchContents} | ||||
|         use:placeholder={Translations.t.general.search.search} | ||||
|       /> | ||||
|       {#if feedback !== undefined} | ||||
|         <!-- The feedback is _always_ shown for screenreaders and to make sure that the searchfield can still be selected by tabbing--> | ||||
|         <div class="alert " role="alert" aria-live="assertive"> | ||||
|           {feedback} | ||||
|         </div> | ||||
|       {/if} | ||||
|     {/if} | ||||
|   </form> | ||||
|   <SearchIcon class="h-6 w-6 self-end" aria-hidden="true" on:click={performSearch}/> | ||||
|   <SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={performSearch} /> | ||||
| </div> | ||||
|  |  | |||
							
								
								
									
										45
									
								
								src/UI/BigComponents/ReverseGeocoding.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/UI/BigComponents/ReverseGeocoding.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | |||
| <script lang="ts">/** | ||||
|  * Shows the current address when shaken | ||||
|  **/ | ||||
| import Motion from "../../Sensors/Motion" | ||||
| import { Geocoding } from "../../Logic/Osm/Geocoding" | ||||
| import type { MapProperties } from "../../Models/MapProperties" | ||||
| 
 | ||||
| export let mapProperties: MapProperties | ||||
| let lastDisplayed: Date = undefined | ||||
| let currentLocation: string = undefined | ||||
| 
 | ||||
| async function displayLocation() { | ||||
|   lastDisplayed = new Date() | ||||
|   let result = await Geocoding.reverse( | ||||
|     mapProperties.location.data, | ||||
|     mapProperties.zoom.data, | ||||
|   ) | ||||
|   console.log("Got result", result) | ||||
|   let properties = result.features[0].properties | ||||
|   currentLocation = properties.display_name | ||||
|   window.setTimeout(() => { | ||||
|     currentLocation = undefined | ||||
|   }, 5000) | ||||
| } | ||||
| 
 | ||||
| Motion.singleton.lastShakeEvent.addCallbackD(shaken => { | ||||
|   console.log("Got a shaken event") | ||||
|   if (shaken.getTime() - lastDisplayed.getTime() < 1000) { | ||||
|     console.log("To soon:",shaken.getTime() - lastDisplayed.getTime()) | ||||
|    // return | ||||
|   } | ||||
|   displayLocation() | ||||
| }) | ||||
| 
 | ||||
| Motion.singleton.startListening() | ||||
| mapProperties.location.stabilized(500).addCallbackAndRun(loc => { | ||||
|   displayLocation() | ||||
| }) | ||||
| </script> | ||||
| 
 | ||||
| {#if currentLocation} | ||||
|   <div role="alert" aria-live="assertive" class="normal-background"> | ||||
|     {currentLocation} | ||||
|   </div> | ||||
| {/if} | ||||
|  | @ -30,10 +30,12 @@ | |||
|   ) | ||||
| </script> | ||||
| 
 | ||||
| <button class="cursor-pointer small flex" on:click={() => select()}> | ||||
| <div class="cursor-pointer small flex" on:click={() => select()}> | ||||
|   <span class="flex"> | ||||
|   {#if i !== undefined} | ||||
|     <span class="font-bold">{i + 1}.</span> | ||||
|   {/if} | ||||
|   <TagRenderingAnswer config={layer.title} {layer} selectedElement={feature} {state} {tags} /> | ||||
|   <span class="flex">{$bearingAndDist.dist}m {$bearingAndDist.bearing}°</span> | ||||
| </button> | ||||
|     <TagRenderingAnswer config={layer.title} {layer} selectedElement={feature} {state} {tags} /> | ||||
|     {$bearingAndDist.dist}m {$bearingAndDist.bearing}° | ||||
|   </span> | ||||
| </div> | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ | |||
|   }) | ||||
|   lastAction.stabilized(750).addCallbackAndRunD(_ => lastAction.setData(undefined)) | ||||
| </script> | ||||
| <div aria-live="assertive" class=" interactive p-1" role="alert"> | ||||
| <div aria-live="assertive" class="p-1" role="alert"> | ||||
| 
 | ||||
|   {#if $lastAction !== undefined} | ||||
|     <Tr t={Translations.t.general.visualFeedback[$lastAction.key]} /> | ||||
|  |  | |||
|  | @ -9,7 +9,6 @@ import SvelteUIElement from "../Base/SvelteUIElement" | |||
| import MaplibreMap from "./MaplibreMap.svelte" | ||||
| import { RasterLayerProperties } from "../../Models/RasterLayerProperties" | ||||
| import * as htmltoimage from "html-to-image" | ||||
| import { ALL } from "node:dns" | ||||
| 
 | ||||
| /** | ||||
|  * The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties` | ||||
|  | @ -41,6 +40,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|     readonly lastClickLocation: Store<undefined | { lon: number; lat: number }> | ||||
|     readonly minzoom: UIEventSource<number> | ||||
|     readonly maxzoom: UIEventSource<number> | ||||
|     readonly rotation: UIEventSource<number> | ||||
|     readonly animationRunning = new UIEventSource(false) | ||||
| 
 | ||||
|     /** | ||||
|      * Functions that are called when one of those actions has happened | ||||
|  | @ -81,6 +82,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|         this.allowRotating = state?.allowRotating ?? new UIEventSource<boolean>(true) | ||||
|         this.allowZooming = state?.allowZooming ?? new UIEventSource(true) | ||||
|         this.bounds = state?.bounds ?? new UIEventSource(undefined) | ||||
|         this.rotation = state?.rotation ?? new UIEventSource<number>(0) | ||||
|         this.rasterLayer = | ||||
|             state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined) | ||||
| 
 | ||||
|  | @ -121,6 +123,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|             self.setMinzoom(self.minzoom.data) | ||||
|             self.setMaxzoom(self.maxzoom.data) | ||||
|             self.setBounds(self.bounds.data) | ||||
|             self.SetRotation(self.rotation.data) | ||||
|             self.setBackground() | ||||
|             this.updateStores(true) | ||||
|             map.on("moveend", () => this.updateStores()) | ||||
|  | @ -133,6 +136,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|             map.on("dblclick", (e) => { | ||||
|                 handleClick(e) | ||||
|             }) | ||||
|             map.on("rotateend", (e) => { | ||||
|                 this.updateStores() | ||||
|             }) | ||||
|             map.getContainer().addEventListener("keydown", (event) => { | ||||
|                 let locked: "islocked" = undefined | ||||
|                 if (!this.allowMoving.data) { | ||||
|  | @ -169,12 +175,12 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|                 console.error("Could not set background") | ||||
|             }) | ||||
|         ) | ||||
| 
 | ||||
|         this.location.addCallbackAndRunD((loc) => { | ||||
|             self.MoveMapToCurrentLoc(loc) | ||||
|         }) | ||||
|         this.zoom.addCallbackAndRunD((z) => self.SetZoom(z)) | ||||
|         this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox)) | ||||
|         this.rotation.addCallbackAndRunD((bearing) => self.SetRotation(bearing)) | ||||
|         this.allowMoving.addCallbackAndRun((allowMoving) => { | ||||
|             self.setAllowMoving(allowMoving) | ||||
|             self.pingKeycodeEvent(allowMoving ? "unlocked" : "locked") | ||||
|  | @ -459,6 +465,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|         if (this.bounds.data === undefined || !isSetup) { | ||||
|             this.bounds.setData(bbox) | ||||
|         } | ||||
|         this.rotation.setData(map.getBearing()) | ||||
|     } | ||||
| 
 | ||||
|     private SetZoom(z: number): void { | ||||
|  | @ -471,6 +478,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private SetRotation(bearing: number): void { | ||||
|         const map = this._maplibreMap.data | ||||
|         if (!map || bearing === undefined) { | ||||
|             return | ||||
|         } | ||||
|         map.rotateTo(bearing, { duration: 0 }) | ||||
|     } | ||||
| 
 | ||||
|     private MoveMapToCurrentLoc(loc: { lat: number; lon: number }): void { | ||||
|         const map = this._maplibreMap.data | ||||
|         if (!map || loc === undefined) { | ||||
|  |  | |||
|  | @ -955,7 +955,6 @@ export class ToTextualDescription { | |||
|         ] | ||||
|         for (let i = 0; i < weekdays.length; i++) { | ||||
|             const day = weekdays[i] | ||||
|             console.log(day, "-->", ranges[i]) | ||||
|             if (ranges[i]?.length > 0) { | ||||
|                 result.push( | ||||
|                     t[day].Subs({ ranges: ToTextualDescription.createRangesFor(ranges[i]) }) | ||||
|  |  | |||
|  | @ -38,7 +38,7 @@ | |||
| 
 | ||||
|   let htmlElem: HTMLDivElement | ||||
|   $: { | ||||
|     if (editMode && htmlElem !== undefined && config.IsKnown(tags)) { | ||||
|     if (editMode && htmlElem !== undefined && config.IsKnown($tags)) { | ||||
|       // EditMode switched to true yet the answer is already known, so the person wants to make a change | ||||
|       // Make sure that the question is in the scrollview! | ||||
| 
 | ||||
|  | @ -108,7 +108,8 @@ | |||
|             editMode = true | ||||
|           }} | ||||
|           class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1" | ||||
|           aria-labelledby={answerId} | ||||
|           aria-labelledby={config.editButtonAriaLabel === undefined ? answerId : undefined} | ||||
|           use:ariaLabel={config.editButtonAriaLabel} | ||||
|         > | ||||
|           <PencilAltIcon /> | ||||
|         </button> | ||||
|  |  | |||
|  | @ -1,10 +1,16 @@ | |||
| <script lang="ts"> | ||||
|   // Testing grounds | ||||
|   import { UIEventSource } from "../Logic/UIEventSource" | ||||
|   import SlopeInput from "./InputElement/Helpers/SlopeInput.svelte" | ||||
|   import OrientationDebugPanel from "./Debug/OrientationDebugPanel.svelte" | ||||
|   let value: UIEventSource<string> = new UIEventSource(undefined) | ||||
|   import Motion from "../Sensors/Motion" | ||||
|   import { Store, Stores } from "../Logic/UIEventSource" | ||||
| 
 | ||||
|   let maxAcc = Motion.singleton.maxAcc | ||||
|   let shaken =Motion.singleton.lastShakeEvent | ||||
|   let recentlyShaken = Stores.Chronic(250).mapD(now => now.getTime() - 3000 < shaken.data?.getTime()) | ||||
| </script> | ||||
| 
 | ||||
| <OrientationDebugPanel/> | ||||
| <SlopeInput /> | ||||
| Acc: {$maxAcc} | ||||
| {#if $recentlyShaken} | ||||
|   <div class="text-red-500 text-5xl"> | ||||
|     SHAKEN | ||||
|   </div> | ||||
|   {/if} | ||||
|  |  | |||
|  | @ -66,6 +66,8 @@ | |||
|   import { Orientation } from "../Sensors/Orientation" | ||||
|   import GeolocationControl from "./BigComponents/GeolocationControl.svelte" | ||||
|   import Compass_arrow from "../assets/svg/Compass_arrow.svelte" | ||||
|   import ReverseGeocoding from "./BigComponents/ReverseGeocoding.svelte" | ||||
|   import FilterPanel from "./BigComponents/FilterPanel.svelte" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
|   let layout = state.layout | ||||
|  | @ -183,6 +185,7 @@ | |||
|     <!-- Flex and w-full are needed for the positioning --> | ||||
|     <!-- Centermessage --> | ||||
|     <StateIndicator {state} /> | ||||
|     <ReverseGeocoding mapProperties={mapproperties}/> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
|  | @ -270,7 +273,7 @@ | |||
|           </MapControlButton> | ||||
|           {#if $compassLoaded} | ||||
|             <div class="absolute top-0 left-0 w-0 h-0 m-0.5 sm:m-1"> | ||||
|               <Compass_arrow class="compass_arrow" style={`rotate: calc(${-$compass}deg + 225deg); transform-origin: 50% 50%;`} /> | ||||
|               <Compass_arrow class="compass_arrow" style={`rotate: calc(${-$compass}deg + 45deg); transform-origin: 50% 50%;`} /> | ||||
|             </div> | ||||
|           {/if} | ||||
|         </div> | ||||
|  | @ -360,55 +363,39 @@ | |||
|       </div> | ||||
| 
 | ||||
|       <div class="flex" slot="title1"> | ||||
|         <Filter class="h-4 w-4" /> | ||||
|         <Tr t={Translations.t.general.menu.filter} /> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="m-2 flex flex-col" slot="content1"> | ||||
|         {#each layout.layers as layer} | ||||
|           <Filterview | ||||
|             zoomlevel={state.mapProperties.zoom} | ||||
|             filteredLayer={state.layerState.filteredLayers.get(layer.id)} | ||||
|             highlightedLayer={state.guistate.highlightedLayerInFilters} | ||||
|           /> | ||||
|         {/each} | ||||
|         {#each layout.tileLayerSources as tilesource} | ||||
|           <OverlayToggle | ||||
|             layerproperties={tilesource} | ||||
|             state={state.overlayLayerStates.get(tilesource.id)} | ||||
|             highlightedLayer={state.guistate.highlightedLayerInFilters} | ||||
|             zoomlevel={state.mapProperties.zoom} | ||||
|           /> | ||||
|         {/each} | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="flex" slot="title2"> | ||||
|         <If condition={state.featureSwitches.featureSwitchEnableExport}> | ||||
|           <Download class="h-4 w-4" /> | ||||
|           <Tr t={Translations.t.general.download.title} /> | ||||
|         </If> | ||||
|       </div> | ||||
|       <div class="m-4" slot="content2"> | ||||
|       <div class="m-4" slot="content1"> | ||||
|         <DownloadPanel {state} /> | ||||
|       </div> | ||||
| 
 | ||||
|       <div slot="title3"> | ||||
|       <div slot="title2"> | ||||
|         <Tr t={Translations.t.general.attribution.title} /> | ||||
|       </div> | ||||
| 
 | ||||
|       <ToSvelte construct={() => new CopyrightPanel(state)} slot="content3" /> | ||||
|       <ToSvelte construct={() => new CopyrightPanel(state)} slot="content2" /> | ||||
| 
 | ||||
|       <div class="flex" slot="title4"> | ||||
|       <div class="flex" slot="title3"> | ||||
|         <Share class="h-4 w-4" /> | ||||
|         <Tr t={Translations.t.general.sharescreen.title} /> | ||||
|       </div> | ||||
|       <div class="m-2" slot="content4"> | ||||
|       <div class="m-2" slot="content3"> | ||||
|         <ShareScreen {state} /> | ||||
|       </div> | ||||
|     </TabbedGroup> | ||||
|   </FloatOver> | ||||
| </If> | ||||
| 
 | ||||
| <If condition={state.guistate.filtersPanelIsOpened}> | ||||
|   <FloatOver on:close={() => state.guistate.filtersPanelIsOpened.setData(false)}> | ||||
|     <FilterPanel {state}/> | ||||
|   </FloatOver> | ||||
| </If> | ||||
| 
 | ||||
| 
 | ||||
| <IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}> | ||||
|   <!-- background layer selector --> | ||||
|   <FloatOver | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| import { Translation } from "../UI/i18n/Translation" | ||||
| import Locale from "../UI/i18n/Locale" | ||||
| 
 | ||||
| export function ariaLabel(htmlElement: Element, t: Translation) { | ||||
|     if (!t) { | ||||
|  | @ -6,6 +7,19 @@ export function ariaLabel(htmlElement: Element, t: Translation) { | |||
|     } | ||||
|     let destroy: () => void = undefined | ||||
| 
 | ||||
|     Locale.language.map((language) => { | ||||
|         if (!t.translations[language]) { | ||||
|             console.log( | ||||
|                 "No aria label in", | ||||
|                 language, | ||||
|                 "for", | ||||
|                 t.context, | ||||
|                 "; en is", | ||||
|                 t.translations["en"] | ||||
|             ) | ||||
|         } | ||||
|     }) | ||||
| 
 | ||||
|     t.current.map( | ||||
|         (label) => { | ||||
|             htmlElement.setAttribute("aria-label", label) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue