forked from MapComplete/MapComplete
		
	UX: add possibility to select map features by only using the keyboard, see #1181
This commit is contained in:
		
							parent
							
								
									7469a0d607
								
							
						
					
					
						commit
						48171d30f5
					
				
					 9 changed files with 304 additions and 104 deletions
				
			
		|  | @ -404,6 +404,7 @@ | |||
|         "key": "Key combination", | ||||
|         "openLayersPanel": "Opens the layers and filters panel", | ||||
|         "selectAerial": "Set the background to aerial or satellite imagery. Toggles between the two best, available layers", | ||||
|         "selectItem": "Select the POI which is closest to the map center (crosshair). Only when in keyboard navigation is used", | ||||
|         "selectMap": "Set the background to a map from external sources. Toggles between the two best, available layers", | ||||
|         "selectMapnik": "Set the background layer to OpenStreetMap-carto", | ||||
|         "selectOsmbasedmap": "Set the background layer to on OpenStreetMap-based map (or disable the background raster layer)", | ||||
|  |  | |||
|  | @ -0,0 +1,94 @@ | |||
| import { FeatureSource } from "../FeatureSource" | ||||
| import { Store, Stores, UIEventSource } from "../../UIEventSource" | ||||
| import { Feature } from "geojson" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import FilteringFeatureSource from "./FilteringFeatureSource" | ||||
| import LayerState from "../../State/LayerState" | ||||
| 
 | ||||
| export default class NearbyFeatureSource implements FeatureSource { | ||||
|     public readonly features: Store<Feature[]> | ||||
|     private readonly _targetPoint: Store<{ lon: number; lat: number }> | ||||
|     private readonly _numberOfNeededFeatures: number | ||||
|     private readonly _currentZoom: Store<number> | ||||
| 
 | ||||
|     constructor( | ||||
|         targetPoint: Store<{ lon: number; lat: number }>, | ||||
|         sources: ReadonlyMap<string, FilteringFeatureSource>, | ||||
|         numberOfNeededFeatures?: number, | ||||
|         layerState?: LayerState, | ||||
|         currentZoom?: Store<number> | ||||
|     ) { | ||||
|         this._targetPoint = targetPoint.stabilized(100) | ||||
|         this._numberOfNeededFeatures = numberOfNeededFeatures | ||||
|         this._currentZoom = currentZoom.stabilized(500) | ||||
| 
 | ||||
|         const allSources: Store<{ feat: Feature; d: number }[]>[] = [] | ||||
|         let minzoom = 999 | ||||
| 
 | ||||
|         const result = new UIEventSource<Feature[]>(undefined) | ||||
|         this.features = Stores.ListStabilized(result) | ||||
| 
 | ||||
|         function update() { | ||||
|             let features: { feat: Feature; d: number }[] = [] | ||||
|             for (const src of allSources) { | ||||
|                 features.push(...src.data) | ||||
|             } | ||||
|             features.sort((a, b) => a.d - b.d) | ||||
|             if (numberOfNeededFeatures !== undefined) { | ||||
|                 features = features.slice(0, numberOfNeededFeatures) | ||||
|             } | ||||
|             result.setData(features.map((f) => f.feat)) | ||||
|         } | ||||
| 
 | ||||
|         sources.forEach((source, layer) => { | ||||
|             const flayer = layerState?.filteredLayers.get(layer) | ||||
|             minzoom = Math.min(minzoom, flayer.layerDef.minzoom) | ||||
|             const calcSource = this.createSource( | ||||
|                 source.features, | ||||
|                 flayer.layerDef.minzoom, | ||||
|                 flayer.isDisplayed | ||||
|             ) | ||||
|             calcSource.addCallbackAndRunD((features) => { | ||||
|                 update() | ||||
|             }) | ||||
|             allSources.push(calcSource) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sorts the given source by distance, slices down to the required number | ||||
|      */ | ||||
|     private createSource( | ||||
|         source: Store<Feature[]>, | ||||
|         minZoom: number, | ||||
|         isActive?: Store<boolean> | ||||
|     ): Store<{ feat: Feature; d: number }[]> { | ||||
|         const empty = [] | ||||
|         return source.stabilized(100).map( | ||||
|             (feats) => { | ||||
|                 if (isActive && !isActive.data) { | ||||
|                     return empty | ||||
|                 } | ||||
| 
 | ||||
|                 if (this._currentZoom.data < minZoom) { | ||||
|                     return empty | ||||
|                 } | ||||
|                 const point = this._targetPoint.data | ||||
|                 const lonLat = <[number, number]>[point.lon, point.lat] | ||||
|                 const withDistance = feats.map((feat) => ({ | ||||
|                     d: GeoOperations.distanceBetween( | ||||
|                         lonLat, | ||||
|                         GeoOperations.centerpointCoordinates(feat) | ||||
|                     ), | ||||
|                     feat, | ||||
|                 })) | ||||
|                 withDistance.sort((a, b) => a.d - b.d) | ||||
|                 if (this._numberOfNeededFeatures !== undefined) { | ||||
|                     return withDistance.slice(0, this._numberOfNeededFeatures) | ||||
|                 } | ||||
|                 return withDistance | ||||
|             }, | ||||
|             [this._targetPoint, isActive, this._currentZoom] | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -12,7 +12,7 @@ import { GlobalFilter } from "./GlobalFilter" | |||
| 
 | ||||
| export default class FilteredLayer { | ||||
|     /** | ||||
|      * Wether or not the specified layer is shown | ||||
|      * Whether or not the specified layer is enabled by the user | ||||
|      */ | ||||
|     readonly isDisplayed: UIEventSource<boolean> | ||||
|     /** | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ export interface MapProperties { | |||
|     readonly allowRotating: UIEventSource<true | boolean> | ||||
|     readonly lastClickLocation: Store<{ lon: number; lat: number }> | ||||
|     readonly allowZooming: UIEventSource<true | boolean> | ||||
|     readonly lastKeyNavigation: UIEventSource<number> | ||||
| } | ||||
| 
 | ||||
| export interface ExportableMap { | ||||
|  |  | |||
|  | @ -57,6 +57,7 @@ import FilteredLayer from "./FilteredLayer" | |||
| import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector" | ||||
| import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" | ||||
| import { Imgur } from "../Logic/ImageProviders/Imgur" | ||||
| import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSource" | ||||
| 
 | ||||
| /** | ||||
|  * | ||||
|  | @ -95,6 +96,10 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|     readonly indexedFeatures: IndexedFeatureSource & LayoutSource | ||||
|     readonly currentView: FeatureSource<Feature<Polygon>> | ||||
|     readonly featuresInView: FeatureSource | ||||
|     /** | ||||
|      * Contains a few (<10) >features that are near the center of the map. | ||||
|      */ | ||||
|     readonly closestFeatures: FeatureSource | ||||
|     readonly newFeatures: WritableFeatureSource | ||||
|     readonly layerState: LayerState | ||||
|     readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | ||||
|  | @ -131,6 +136,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         this.map = new UIEventSource<MlMap>(undefined) | ||||
|         const initial = new InitialMapPositioning(layout) | ||||
|         this.mapProperties = new MapLibreAdaptor(this.map, initial) | ||||
| 
 | ||||
|         const geolocationState = new GeoLocationState() | ||||
| 
 | ||||
|         this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting | ||||
|  | @ -234,6 +240,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|                 }) | ||||
|             ) | ||||
|             this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) | ||||
| 
 | ||||
|             this.dataIsLoading = layoutSource.isLoading | ||||
| 
 | ||||
|             const indexedElements = this.indexedFeatures | ||||
|  | @ -331,7 +338,13 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         ) | ||||
| 
 | ||||
|         this.perLayerFiltered = this.showNormalDataOn(this.map) | ||||
| 
 | ||||
|         this.closestFeatures = new NearbyFeatureSource( | ||||
|             this.mapProperties.location, | ||||
|             this.perLayerFiltered, | ||||
|             3, | ||||
|             this.layerState, | ||||
|             this.mapProperties.zoom | ||||
|         ) | ||||
|         this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView | ||||
|         this.imageUploadManager = new ImageUploadManager( | ||||
|             layout, | ||||
|  | @ -364,6 +377,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|             return true | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> { | ||||
|         const filteringFeatureSource = new Map<string, FilteringFeatureSource>() | ||||
|         this.perLayer.forEach((fs, layerName) => { | ||||
|  | @ -404,6 +418,17 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         return filteringFeatureSource | ||||
|     } | ||||
| 
 | ||||
|     public openNewDialog() { | ||||
|         this.selectedLayer.setData(undefined) | ||||
|         this.selectedElement.setData(undefined) | ||||
| 
 | ||||
|         const { lon, lat } = this.mapProperties.location.data | ||||
|         const feature = this.lastClickObject.createFeature(lon, lat) | ||||
|         this.featureProperties.trackFeature(feature) | ||||
|         this.selectedElement.setData(feature) | ||||
|         this.selectedLayer.setData(this.newPointDialog.layerDef) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Various small methods that need to be called | ||||
|      */ | ||||
|  | @ -425,6 +450,21 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Selects the feature that is 'i' closest to the map center | ||||
|      * @param i | ||||
|      * @private | ||||
|      */ | ||||
|     private selectClosestAtCenter(i: number = 0) { | ||||
|         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) | ||||
|     } | ||||
|     private initHotkeys() { | ||||
|         Hotkeys.RegisterHotkey( | ||||
|             { nomod: "Escape", onUp: true }, | ||||
|  | @ -436,6 +476,36 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         this.mapProperties.lastKeyNavigation.addCallbackAndRunD((_) => { | ||||
|             Hotkeys.RegisterHotkey( | ||||
|                 { | ||||
|                     nomod: " ", | ||||
|                     onUp: true, | ||||
|                 }, | ||||
|                 Translations.t.hotkeyDocumentation.selectItem, | ||||
|                 () => this.selectClosestAtCenter(0) | ||||
|             ) | ||||
|             Hotkeys.RegisterHotkey( | ||||
|                 { | ||||
|                     nomod: "Spacebar", | ||||
|                     onUp: true, | ||||
|                 }, | ||||
|                 Translations.t.hotkeyDocumentation.selectItem, | ||||
|                 () => this.selectClosestAtCenter(0) | ||||
|             ) | ||||
|             for (let i = 1; i < 9; i++) { | ||||
|                 Hotkeys.RegisterHotkey( | ||||
|                     { | ||||
|                         nomod: "" + i, | ||||
|                         onUp: true, | ||||
|                     }, | ||||
|                     Translations.t.hotkeyDocumentation.selectItem, | ||||
|                     () => this.selectClosestAtCenter(i - 1) | ||||
|                 ) | ||||
|             } | ||||
|             return true // unregister
 | ||||
|         }) | ||||
| 
 | ||||
|         this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => { | ||||
|             if (!enable) { | ||||
|                 return | ||||
|  | @ -531,17 +601,6 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public openNewDialog() { | ||||
|         this.selectedLayer.setData(undefined) | ||||
|         this.selectedElement.setData(undefined) | ||||
| 
 | ||||
|         const { lon, lat } = this.mapProperties.location.data | ||||
|         const feature = this.lastClickObject.createFeature(lon, lat) | ||||
|         this.featureProperties.trackFeature(feature) | ||||
|         this.selectedElement.setData(feature) | ||||
|         this.selectedLayer.setData(this.newPointDialog.layerDef) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add the special layers to the map | ||||
|      */ | ||||
|  |  | |||
|  | @ -38,9 +38,9 @@ | |||
|     <div class="flex flex-col"> | ||||
|       <!-- Title element--> | ||||
|       <h3> | ||||
| 
 | ||||
|         <TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} /> | ||||
|       </h3> | ||||
| 
 | ||||
|       <div | ||||
|         class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 p-1 pt-0.5 sm:pt-1" | ||||
|       > | ||||
|  |  | |||
|  | @ -0,0 +1,16 @@ | |||
| <script lang="ts"> | ||||
|   import type { Feature } from "geojson"; | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
|   import SelectedElementTitle from "./SelectedElementTitle.svelte"; | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
|   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"; | ||||
| 
 | ||||
|   export let state: SpecialVisualizationState; | ||||
|   export let feature: Feature | ||||
|   let id = feature.properties.id | ||||
|   let tags = state.featureProperties.getStore(id); | ||||
|   let layer: LayerConfig = state.layout.getMatchingLayer(tags.data) | ||||
|    | ||||
| </script> | ||||
| <TagRenderingAnswer config={layer.title} selectedElement={feature} {state} {tags} {layer} /> | ||||
| 
 | ||||
|  | @ -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 { draw } from "svelte/transition" | ||||
| 
 | ||||
| /** | ||||
|  * The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties` | ||||
|  | @ -41,6 +40,12 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|     readonly lastClickLocation: Store<undefined | { lon: number; lat: number }> | ||||
|     readonly minzoom: UIEventSource<number> | ||||
|     readonly maxzoom: UIEventSource<number> | ||||
|     /** | ||||
|      * When was the last navigation by arrow keys? | ||||
|      * If set, this is a hint to use arrow compatibility | ||||
|      * Number of _seconds_ since epoch | ||||
|      */ | ||||
|     readonly lastKeyNavigation: UIEventSource<number> = new UIEventSource<number>(undefined) | ||||
|     private readonly _maplibreMap: Store<MLMap> | ||||
|     /** | ||||
|      * Used for internal bookkeeping (to remove a rasterLayer when done loading) | ||||
|  | @ -128,6 +133,16 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|             map.on("dblclick", (e) => { | ||||
|                 handleClick(e) | ||||
|             }) | ||||
|             map.getContainer().addEventListener("keydown", (event) => { | ||||
|                 if ( | ||||
|                     event.key === "ArrowRight" || | ||||
|                     event.key === "ArrowLeft" || | ||||
|                     event.key === "ArrowUp" || | ||||
|                     event.key === "ArrowDown" | ||||
|                 ) { | ||||
|                     this.lastKeyNavigation.setData(Date.now() / 1000) | ||||
|                 } | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         this.rasterLayer.addCallbackAndRun((_) => | ||||
|  |  | |||
|  | @ -1,120 +1,127 @@ | |||
| <script lang="ts"> | ||||
|   import { Store, UIEventSource } from "../Logic/UIEventSource" | ||||
|   import { Map as MlMap } from "maplibre-gl" | ||||
|   import MaplibreMap from "./Map/MaplibreMap.svelte" | ||||
|   import FeatureSwitchState from "../Logic/State/FeatureSwitchState" | ||||
|   import MapControlButton from "./Base/MapControlButton.svelte" | ||||
|   import ToSvelte from "./Base/ToSvelte.svelte" | ||||
|   import If from "./Base/If.svelte" | ||||
|   import { GeolocationControl } from "./BigComponents/GeolocationControl" | ||||
|   import type { Feature } from "geojson" | ||||
|   import SelectedElementView from "./BigComponents/SelectedElementView.svelte" | ||||
|   import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||
|   import Filterview from "./BigComponents/Filterview.svelte" | ||||
|   import ThemeViewState from "../Models/ThemeViewState" | ||||
|   import type { MapProperties } from "../Models/MapProperties" | ||||
|   import Geosearch from "./BigComponents/Geosearch.svelte" | ||||
|   import Translations from "./i18n/Translations" | ||||
|   import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import Tr from "./Base/Tr.svelte" | ||||
|   import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte" | ||||
|   import FloatOver from "./Base/FloatOver.svelte" | ||||
|   import PrivacyPolicy from "./BigComponents/PrivacyPolicy" | ||||
|   import Constants from "../Models/Constants" | ||||
|   import TabbedGroup from "./Base/TabbedGroup.svelte" | ||||
|   import UserRelatedState from "../Logic/State/UserRelatedState" | ||||
|   import LoginToggle from "./Base/LoginToggle.svelte" | ||||
|   import LoginButton from "./Base/LoginButton.svelte" | ||||
|   import CopyrightPanel from "./BigComponents/CopyrightPanel" | ||||
|   import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte" | ||||
|   import ModalRight from "./Base/ModalRight.svelte" | ||||
|   import { Utils } from "../Utils" | ||||
|   import Hotkeys from "./Base/Hotkeys" | ||||
|   import { VariableUiElement } from "./Base/VariableUIElement" | ||||
|   import SvelteUIElement from "./Base/SvelteUIElement" | ||||
|   import OverlayToggle from "./BigComponents/OverlayToggle.svelte" | ||||
|   import LevelSelector from "./BigComponents/LevelSelector.svelte" | ||||
|   import ExtraLinkButton from "./BigComponents/ExtraLinkButton" | ||||
|   import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte" | ||||
|   import Svg from "../Svg" | ||||
|   import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte" | ||||
|   import type { RasterLayerPolygon } from "../Models/RasterLayers" | ||||
|   import { AvailableRasterLayers } from "../Models/RasterLayers" | ||||
|   import RasterLayerOverview from "./Map/RasterLayerOverview.svelte" | ||||
|   import IfHidden from "./Base/IfHidden.svelte" | ||||
|   import { onDestroy } from "svelte" | ||||
|   import { OpenJosm } from "./BigComponents/OpenJosm" | ||||
|   import MapillaryLink from "./BigComponents/MapillaryLink.svelte" | ||||
|   import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte" | ||||
|   import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte" | ||||
|   import StateIndicator from "./BigComponents/StateIndicator.svelte" | ||||
|   import LanguagePicker from "./LanguagePicker" | ||||
|   import Locale from "./i18n/Locale" | ||||
|   import ShareScreen from "./BigComponents/ShareScreen.svelte" | ||||
|   import UploadingImageCounter from "./Image/UploadingImageCounter.svelte" | ||||
|   import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte" | ||||
|   import { Store, UIEventSource } from "../Logic/UIEventSource"; | ||||
|   import { Map as MlMap } from "maplibre-gl"; | ||||
|   import MaplibreMap from "./Map/MaplibreMap.svelte"; | ||||
|   import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; | ||||
|   import MapControlButton from "./Base/MapControlButton.svelte"; | ||||
|   import ToSvelte from "./Base/ToSvelte.svelte"; | ||||
|   import If from "./Base/If.svelte"; | ||||
|   import { GeolocationControl } from "./BigComponents/GeolocationControl"; | ||||
|   import type { Feature } from "geojson"; | ||||
|   import SelectedElementView from "./BigComponents/SelectedElementView.svelte"; | ||||
|   import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
|   import Filterview from "./BigComponents/Filterview.svelte"; | ||||
|   import ThemeViewState from "../Models/ThemeViewState"; | ||||
|   import type { MapProperties } from "../Models/MapProperties"; | ||||
|   import Geosearch from "./BigComponents/Geosearch.svelte"; | ||||
|   import Translations from "./i18n/Translations"; | ||||
|   import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import Tr from "./Base/Tr.svelte"; | ||||
|   import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"; | ||||
|   import FloatOver from "./Base/FloatOver.svelte"; | ||||
|   import PrivacyPolicy from "./BigComponents/PrivacyPolicy"; | ||||
|   import Constants from "../Models/Constants"; | ||||
|   import TabbedGroup from "./Base/TabbedGroup.svelte"; | ||||
|   import UserRelatedState from "../Logic/State/UserRelatedState"; | ||||
|   import LoginToggle from "./Base/LoginToggle.svelte"; | ||||
|   import LoginButton from "./Base/LoginButton.svelte"; | ||||
|   import CopyrightPanel from "./BigComponents/CopyrightPanel"; | ||||
|   import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"; | ||||
|   import ModalRight from "./Base/ModalRight.svelte"; | ||||
|   import { Utils } from "../Utils"; | ||||
|   import Hotkeys from "./Base/Hotkeys"; | ||||
|   import { VariableUiElement } from "./Base/VariableUIElement"; | ||||
|   import SvelteUIElement from "./Base/SvelteUIElement"; | ||||
|   import OverlayToggle from "./BigComponents/OverlayToggle.svelte"; | ||||
|   import LevelSelector from "./BigComponents/LevelSelector.svelte"; | ||||
|   import ExtraLinkButton from "./BigComponents/ExtraLinkButton"; | ||||
|   import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"; | ||||
|   import Svg from "../Svg"; | ||||
|   import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"; | ||||
|   import type { RasterLayerPolygon } from "../Models/RasterLayers"; | ||||
|   import { AvailableRasterLayers } from "../Models/RasterLayers"; | ||||
|   import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"; | ||||
|   import IfHidden from "./Base/IfHidden.svelte"; | ||||
|   import { onDestroy } from "svelte"; | ||||
|   import { OpenJosm } from "./BigComponents/OpenJosm"; | ||||
|   import MapillaryLink from "./BigComponents/MapillaryLink.svelte"; | ||||
|   import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"; | ||||
|   import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"; | ||||
|   import StateIndicator from "./BigComponents/StateIndicator.svelte"; | ||||
|   import LanguagePicker from "./LanguagePicker"; | ||||
|   import Locale from "./i18n/Locale"; | ||||
|   import ShareScreen from "./BigComponents/ShareScreen.svelte"; | ||||
|   import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"; | ||||
|   import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"; | ||||
|   import Cross from "../assets/svg/Cross.svelte"; | ||||
|   import Summary from "./BigComponents/Summary.svelte"; | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
|   let layout = state.layout | ||||
|   export let state: ThemeViewState; | ||||
|   let layout = state.layout; | ||||
| 
 | ||||
|   let maplibremap: UIEventSource<MlMap> = state.map | ||||
|   let selectedElement: UIEventSource<Feature> = state.selectedElement | ||||
|   let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer | ||||
|   let maplibremap: UIEventSource<MlMap> = state.map; | ||||
|   let selectedElement: UIEventSource<Feature> = state.selectedElement; | ||||
|   let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer; | ||||
| 
 | ||||
|   let currentZoom = state.mapProperties.zoom; | ||||
|   let showCrosshair = state.userRelatedState.showCrosshair; | ||||
|   let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation; | ||||
|   let centerFeatures = state.closestFeatures.features; | ||||
|   $: console.log("Centerfeatures are", $centerFeatures) | ||||
|   const selectedElementView = selectedElement.map( | ||||
|     (selectedElement) => { | ||||
|       // Svelte doesn't properly reload some of the legacy UI-elements | ||||
|       // As such, we _reconstruct_ the selectedElementView every time a new feature is selected | ||||
|       // This is a bit wasteful, but until everything is a svelte-component, this should do the trick | ||||
|       const layer = selectedLayer.data | ||||
|       const layer = selectedLayer.data; | ||||
|       if (selectedElement === undefined || layer === undefined) { | ||||
|         return undefined | ||||
|         return undefined; | ||||
|       } | ||||
| 
 | ||||
|       if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) { | ||||
|         return undefined | ||||
|         return undefined; | ||||
|       } | ||||
| 
 | ||||
|       const tags = state.featureProperties.getStore(selectedElement.properties.id) | ||||
|       const tags = state.featureProperties.getStore(selectedElement.properties.id); | ||||
|       return new SvelteUIElement(SelectedElementView, { | ||||
|         state, | ||||
|         layer, | ||||
|         selectedElement, | ||||
|         tags, | ||||
|       }).SetClass("h-full w-full") | ||||
|         tags | ||||
|       }).SetClass("h-full w-full"); | ||||
|     }, | ||||
|     [selectedLayer] | ||||
|   ) | ||||
|   ); | ||||
| 
 | ||||
|   const selectedElementTitle = selectedElement.map( | ||||
|     (selectedElement) => { | ||||
|       // Svelte doesn't properly reload some of the legacy UI-elements | ||||
|       // As such, we _reconstruct_ the selectedElementView every time a new feature is selected | ||||
|       // This is a bit wasteful, but until everything is a svelte-component, this should do the trick | ||||
|       const layer = selectedLayer.data | ||||
|       const layer = selectedLayer.data; | ||||
|       if (selectedElement === undefined || layer === undefined) { | ||||
|         return undefined | ||||
|         return undefined; | ||||
|       } | ||||
| 
 | ||||
|       const tags = state.featureProperties.getStore(selectedElement.properties.id) | ||||
|       return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags }) | ||||
|       const tags = state.featureProperties.getStore(selectedElement.properties.id); | ||||
|       return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags }); | ||||
|     }, | ||||
|     [selectedLayer] | ||||
|   ) | ||||
|   ); | ||||
| 
 | ||||
|   let mapproperties: MapProperties = state.mapProperties | ||||
|   let featureSwitches: FeatureSwitchState = state.featureSwitches | ||||
|   let availableLayers = state.availableLayers | ||||
|   let userdetails = state.osmConnection.userDetails | ||||
|   let currentViewLayer = layout.layers.find((l) => l.id === "current_view") | ||||
|   let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer | ||||
|   let mapproperties: MapProperties = state.mapProperties; | ||||
|   let featureSwitches: FeatureSwitchState = state.featureSwitches; | ||||
|   let availableLayers = state.availableLayers; | ||||
|   let userdetails = state.osmConnection.userDetails; | ||||
|   let currentViewLayer = layout.layers.find((l) => l.id === "current_view"); | ||||
|   let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer; | ||||
|   let rasterLayerName = | ||||
|     rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name | ||||
|     rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name; | ||||
|   onDestroy( | ||||
|     rasterLayer.addCallbackAndRunD((l) => { | ||||
|       rasterLayerName = l.properties.name | ||||
|       rasterLayerName = l.properties.name; | ||||
|     }) | ||||
|   ) | ||||
|   ); | ||||
| </script> | ||||
| 
 | ||||
| <div class="absolute top-0 left-0 h-screen w-screen overflow-hidden"> | ||||
|  | @ -216,6 +223,15 @@ | |||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     {#if $arrowKeysWereUsed !== undefined} | ||||
|       <div class="pointer-events-auto interactive p-1"> | ||||
|         {#each $centerFeatures as feat, i (feat.properties.id)} | ||||
|           <div class="flex"> | ||||
|           <b>{i+1}.</b><Summary {state} feature={feat}/> | ||||
|           </div> | ||||
|         {/each} | ||||
|       </div> | ||||
|     {/if} | ||||
|     <div class="flex flex-col items-end"> | ||||
|       <!-- bottom right elements --> | ||||
|       <If condition={state.floors.map((f) => f.length > 1)}> | ||||
|  | @ -247,15 +263,13 @@ | |||
| </div> | ||||
| 
 | ||||
| <LoginToggle ignoreLoading={true} {state}> | ||||
|   <If condition={state.userRelatedState.showCrosshair.map((s) => s === "yes")}> | ||||
|     <If condition={state.mapProperties.zoom.map((z) => z >= 17)}> | ||||
|   {#if $showCrosshair === "yes" && ($currentZoom >= 17 || $arrowKeysWereUsed !== undefined) } | ||||
|     <div | ||||
|       class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center justify-center" | ||||
|     > | ||||
|         <ToSvelte construct={Svg.cross_svg()} /> | ||||
|       <Cross class="h-4 w-4" /> | ||||
|     </div> | ||||
|     </If> | ||||
|   </If> | ||||
|   {/if} | ||||
| </LoginToggle> | ||||
| 
 | ||||
| <If | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue