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", |         "key": "Key combination", | ||||||
|         "openLayersPanel": "Opens the layers and filters panel", |         "openLayersPanel": "Opens the layers and filters panel", | ||||||
|         "selectAerial": "Set the background to aerial or satellite imagery. Toggles between the two best, available layers", |         "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", |         "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", |         "selectMapnik": "Set the background layer to OpenStreetMap-carto", | ||||||
|         "selectOsmbasedmap": "Set the background layer to on OpenStreetMap-based map (or disable the background raster layer)", |         "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 { | 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> |     readonly isDisplayed: UIEventSource<boolean> | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ export interface MapProperties { | ||||||
|     readonly allowRotating: UIEventSource<true | boolean> |     readonly allowRotating: UIEventSource<true | boolean> | ||||||
|     readonly lastClickLocation: Store<{ lon: number; lat: number }> |     readonly lastClickLocation: Store<{ lon: number; lat: number }> | ||||||
|     readonly allowZooming: UIEventSource<true | boolean> |     readonly allowZooming: UIEventSource<true | boolean> | ||||||
|  |     readonly lastKeyNavigation: UIEventSource<number> | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ExportableMap { | export interface ExportableMap { | ||||||
|  |  | ||||||
|  | @ -57,6 +57,7 @@ import FilteredLayer from "./FilteredLayer" | ||||||
| import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector" | import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector" | ||||||
| import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" | import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" | ||||||
| import { Imgur } from "../Logic/ImageProviders/Imgur" | 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 indexedFeatures: IndexedFeatureSource & LayoutSource | ||||||
|     readonly currentView: FeatureSource<Feature<Polygon>> |     readonly currentView: FeatureSource<Feature<Polygon>> | ||||||
|     readonly featuresInView: FeatureSource |     readonly featuresInView: FeatureSource | ||||||
|  |     /** | ||||||
|  |      * Contains a few (<10) >features that are near the center of the map. | ||||||
|  |      */ | ||||||
|  |     readonly closestFeatures: FeatureSource | ||||||
|     readonly newFeatures: WritableFeatureSource |     readonly newFeatures: WritableFeatureSource | ||||||
|     readonly layerState: LayerState |     readonly layerState: LayerState | ||||||
|     readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> |     readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | ||||||
|  | @ -131,6 +136,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|         this.map = new UIEventSource<MlMap>(undefined) |         this.map = new UIEventSource<MlMap>(undefined) | ||||||
|         const initial = new InitialMapPositioning(layout) |         const initial = new InitialMapPositioning(layout) | ||||||
|         this.mapProperties = new MapLibreAdaptor(this.map, initial) |         this.mapProperties = new MapLibreAdaptor(this.map, initial) | ||||||
|  | 
 | ||||||
|         const geolocationState = new GeoLocationState() |         const geolocationState = new GeoLocationState() | ||||||
| 
 | 
 | ||||||
|         this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting |         this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting | ||||||
|  | @ -234,6 +240,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|                 }) |                 }) | ||||||
|             ) |             ) | ||||||
|             this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) |             this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) | ||||||
|  | 
 | ||||||
|             this.dataIsLoading = layoutSource.isLoading |             this.dataIsLoading = layoutSource.isLoading | ||||||
| 
 | 
 | ||||||
|             const indexedElements = this.indexedFeatures |             const indexedElements = this.indexedFeatures | ||||||
|  | @ -331,7 +338,13 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         this.perLayerFiltered = this.showNormalDataOn(this.map) |         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.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView | ||||||
|         this.imageUploadManager = new ImageUploadManager( |         this.imageUploadManager = new ImageUploadManager( | ||||||
|             layout, |             layout, | ||||||
|  | @ -364,6 +377,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             return true |             return true | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> { |     public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> { | ||||||
|         const filteringFeatureSource = new Map<string, FilteringFeatureSource>() |         const filteringFeatureSource = new Map<string, FilteringFeatureSource>() | ||||||
|         this.perLayer.forEach((fs, layerName) => { |         this.perLayer.forEach((fs, layerName) => { | ||||||
|  | @ -404,6 +418,17 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|         return filteringFeatureSource |         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 |      * 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() { |     private initHotkeys() { | ||||||
|         Hotkeys.RegisterHotkey( |         Hotkeys.RegisterHotkey( | ||||||
|             { nomod: "Escape", onUp: true }, |             { 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) => { |         this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => { | ||||||
|             if (!enable) { |             if (!enable) { | ||||||
|                 return |                 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 |      * Add the special layers to the map | ||||||
|      */ |      */ | ||||||
|  |  | ||||||
|  | @ -38,9 +38,9 @@ | ||||||
|     <div class="flex flex-col"> |     <div class="flex flex-col"> | ||||||
|       <!-- Title element--> |       <!-- Title element--> | ||||||
|       <h3> |       <h3> | ||||||
|  | 
 | ||||||
|         <TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} /> |         <TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} /> | ||||||
|       </h3> |       </h3> | ||||||
| 
 |  | ||||||
|       <div |       <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" |         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 MaplibreMap from "./MaplibreMap.svelte" | ||||||
| import { RasterLayerProperties } from "../../Models/RasterLayerProperties" | import { RasterLayerProperties } from "../../Models/RasterLayerProperties" | ||||||
| import * as htmltoimage from "html-to-image" | import * as htmltoimage from "html-to-image" | ||||||
| import { draw } from "svelte/transition" |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties` |  * 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 lastClickLocation: Store<undefined | { lon: number; lat: number }> | ||||||
|     readonly minzoom: UIEventSource<number> |     readonly minzoom: UIEventSource<number> | ||||||
|     readonly maxzoom: 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> |     private readonly _maplibreMap: Store<MLMap> | ||||||
|     /** |     /** | ||||||
|      * Used for internal bookkeeping (to remove a rasterLayer when done loading) |      * 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) => { |             map.on("dblclick", (e) => { | ||||||
|                 handleClick(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((_) => |         this.rasterLayer.addCallbackAndRun((_) => | ||||||
|  |  | ||||||
|  | @ -1,120 +1,127 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { Store, UIEventSource } from "../Logic/UIEventSource" |   import { Store, UIEventSource } from "../Logic/UIEventSource"; | ||||||
|   import { Map as MlMap } from "maplibre-gl" |   import { Map as MlMap } from "maplibre-gl"; | ||||||
|   import MaplibreMap from "./Map/MaplibreMap.svelte" |   import MaplibreMap from "./Map/MaplibreMap.svelte"; | ||||||
|   import FeatureSwitchState from "../Logic/State/FeatureSwitchState" |   import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; | ||||||
|   import MapControlButton from "./Base/MapControlButton.svelte" |   import MapControlButton from "./Base/MapControlButton.svelte"; | ||||||
|   import ToSvelte from "./Base/ToSvelte.svelte" |   import ToSvelte from "./Base/ToSvelte.svelte"; | ||||||
|   import If from "./Base/If.svelte" |   import If from "./Base/If.svelte"; | ||||||
|   import { GeolocationControl } from "./BigComponents/GeolocationControl" |   import { GeolocationControl } from "./BigComponents/GeolocationControl"; | ||||||
|   import type { Feature } from "geojson" |   import type { Feature } from "geojson"; | ||||||
|   import SelectedElementView from "./BigComponents/SelectedElementView.svelte" |   import SelectedElementView from "./BigComponents/SelectedElementView.svelte"; | ||||||
|   import LayerConfig from "../Models/ThemeConfig/LayerConfig" |   import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||||
|   import Filterview from "./BigComponents/Filterview.svelte" |   import Filterview from "./BigComponents/Filterview.svelte"; | ||||||
|   import ThemeViewState from "../Models/ThemeViewState" |   import ThemeViewState from "../Models/ThemeViewState"; | ||||||
|   import type { MapProperties } from "../Models/MapProperties" |   import type { MapProperties } from "../Models/MapProperties"; | ||||||
|   import Geosearch from "./BigComponents/Geosearch.svelte" |   import Geosearch from "./BigComponents/Geosearch.svelte"; | ||||||
|   import Translations from "./i18n/Translations" |   import Translations from "./i18n/Translations"; | ||||||
|   import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid" |   import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||||
|   import Tr from "./Base/Tr.svelte" |   import Tr from "./Base/Tr.svelte"; | ||||||
|   import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte" |   import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"; | ||||||
|   import FloatOver from "./Base/FloatOver.svelte" |   import FloatOver from "./Base/FloatOver.svelte"; | ||||||
|   import PrivacyPolicy from "./BigComponents/PrivacyPolicy" |   import PrivacyPolicy from "./BigComponents/PrivacyPolicy"; | ||||||
|   import Constants from "../Models/Constants" |   import Constants from "../Models/Constants"; | ||||||
|   import TabbedGroup from "./Base/TabbedGroup.svelte" |   import TabbedGroup from "./Base/TabbedGroup.svelte"; | ||||||
|   import UserRelatedState from "../Logic/State/UserRelatedState" |   import UserRelatedState from "../Logic/State/UserRelatedState"; | ||||||
|   import LoginToggle from "./Base/LoginToggle.svelte" |   import LoginToggle from "./Base/LoginToggle.svelte"; | ||||||
|   import LoginButton from "./Base/LoginButton.svelte" |   import LoginButton from "./Base/LoginButton.svelte"; | ||||||
|   import CopyrightPanel from "./BigComponents/CopyrightPanel" |   import CopyrightPanel from "./BigComponents/CopyrightPanel"; | ||||||
|   import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte" |   import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"; | ||||||
|   import ModalRight from "./Base/ModalRight.svelte" |   import ModalRight from "./Base/ModalRight.svelte"; | ||||||
|   import { Utils } from "../Utils" |   import { Utils } from "../Utils"; | ||||||
|   import Hotkeys from "./Base/Hotkeys" |   import Hotkeys from "./Base/Hotkeys"; | ||||||
|   import { VariableUiElement } from "./Base/VariableUIElement" |   import { VariableUiElement } from "./Base/VariableUIElement"; | ||||||
|   import SvelteUIElement from "./Base/SvelteUIElement" |   import SvelteUIElement from "./Base/SvelteUIElement"; | ||||||
|   import OverlayToggle from "./BigComponents/OverlayToggle.svelte" |   import OverlayToggle from "./BigComponents/OverlayToggle.svelte"; | ||||||
|   import LevelSelector from "./BigComponents/LevelSelector.svelte" |   import LevelSelector from "./BigComponents/LevelSelector.svelte"; | ||||||
|   import ExtraLinkButton from "./BigComponents/ExtraLinkButton" |   import ExtraLinkButton from "./BigComponents/ExtraLinkButton"; | ||||||
|   import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte" |   import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"; | ||||||
|   import Svg from "../Svg" |   import Svg from "../Svg"; | ||||||
|   import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte" |   import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"; | ||||||
|   import type { RasterLayerPolygon } from "../Models/RasterLayers" |   import type { RasterLayerPolygon } from "../Models/RasterLayers"; | ||||||
|   import { AvailableRasterLayers } from "../Models/RasterLayers" |   import { AvailableRasterLayers } from "../Models/RasterLayers"; | ||||||
|   import RasterLayerOverview from "./Map/RasterLayerOverview.svelte" |   import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"; | ||||||
|   import IfHidden from "./Base/IfHidden.svelte" |   import IfHidden from "./Base/IfHidden.svelte"; | ||||||
|   import { onDestroy } from "svelte" |   import { onDestroy } from "svelte"; | ||||||
|   import { OpenJosm } from "./BigComponents/OpenJosm" |   import { OpenJosm } from "./BigComponents/OpenJosm"; | ||||||
|   import MapillaryLink from "./BigComponents/MapillaryLink.svelte" |   import MapillaryLink from "./BigComponents/MapillaryLink.svelte"; | ||||||
|   import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte" |   import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"; | ||||||
|   import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte" |   import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"; | ||||||
|   import StateIndicator from "./BigComponents/StateIndicator.svelte" |   import StateIndicator from "./BigComponents/StateIndicator.svelte"; | ||||||
|   import LanguagePicker from "./LanguagePicker" |   import LanguagePicker from "./LanguagePicker"; | ||||||
|   import Locale from "./i18n/Locale" |   import Locale from "./i18n/Locale"; | ||||||
|   import ShareScreen from "./BigComponents/ShareScreen.svelte" |   import ShareScreen from "./BigComponents/ShareScreen.svelte"; | ||||||
|   import UploadingImageCounter from "./Image/UploadingImageCounter.svelte" |   import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"; | ||||||
|   import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte" |   import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"; | ||||||
|  |   import Cross from "../assets/svg/Cross.svelte"; | ||||||
|  |   import Summary from "./BigComponents/Summary.svelte"; | ||||||
| 
 | 
 | ||||||
|   export let state: ThemeViewState |   export let state: ThemeViewState; | ||||||
|   let layout = state.layout |   let layout = state.layout; | ||||||
| 
 | 
 | ||||||
|   let maplibremap: UIEventSource<MlMap> = state.map |   let maplibremap: UIEventSource<MlMap> = state.map; | ||||||
|   let selectedElement: UIEventSource<Feature> = state.selectedElement |   let selectedElement: UIEventSource<Feature> = state.selectedElement; | ||||||
|   let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer |   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( |   const selectedElementView = selectedElement.map( | ||||||
|     (selectedElement) => { |     (selectedElement) => { | ||||||
|       // Svelte doesn't properly reload some of the legacy UI-elements |       // Svelte doesn't properly reload some of the legacy UI-elements | ||||||
|       // As such, we _reconstruct_ the selectedElementView every time a new feature is selected |       // 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 |       // 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) { |       if (selectedElement === undefined || layer === undefined) { | ||||||
|         return undefined |         return undefined; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (!(layer.tagRenderings?.length > 0) || layer.title === 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, { |       return new SvelteUIElement(SelectedElementView, { | ||||||
|         state, |         state, | ||||||
|         layer, |         layer, | ||||||
|         selectedElement, |         selectedElement, | ||||||
|         tags, |         tags | ||||||
|       }).SetClass("h-full w-full") |       }).SetClass("h-full w-full"); | ||||||
|     }, |     }, | ||||||
|     [selectedLayer] |     [selectedLayer] | ||||||
|   ) |   ); | ||||||
| 
 | 
 | ||||||
|   const selectedElementTitle = selectedElement.map( |   const selectedElementTitle = selectedElement.map( | ||||||
|     (selectedElement) => { |     (selectedElement) => { | ||||||
|       // Svelte doesn't properly reload some of the legacy UI-elements |       // Svelte doesn't properly reload some of the legacy UI-elements | ||||||
|       // As such, we _reconstruct_ the selectedElementView every time a new feature is selected |       // 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 |       // 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) { |       if (selectedElement === undefined || layer === undefined) { | ||||||
|         return undefined |         return undefined; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const tags = state.featureProperties.getStore(selectedElement.properties.id) |       const tags = state.featureProperties.getStore(selectedElement.properties.id); | ||||||
|       return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags }) |       return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags }); | ||||||
|     }, |     }, | ||||||
|     [selectedLayer] |     [selectedLayer] | ||||||
|   ) |   ); | ||||||
| 
 | 
 | ||||||
|   let mapproperties: MapProperties = state.mapProperties |   let mapproperties: MapProperties = state.mapProperties; | ||||||
|   let featureSwitches: FeatureSwitchState = state.featureSwitches |   let featureSwitches: FeatureSwitchState = state.featureSwitches; | ||||||
|   let availableLayers = state.availableLayers |   let availableLayers = state.availableLayers; | ||||||
|   let userdetails = state.osmConnection.userDetails |   let userdetails = state.osmConnection.userDetails; | ||||||
|   let currentViewLayer = layout.layers.find((l) => l.id === "current_view") |   let currentViewLayer = layout.layers.find((l) => l.id === "current_view"); | ||||||
|   let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer |   let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer; | ||||||
|   let rasterLayerName = |   let rasterLayerName = | ||||||
|     rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name |     rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name; | ||||||
|   onDestroy( |   onDestroy( | ||||||
|     rasterLayer.addCallbackAndRunD((l) => { |     rasterLayer.addCallbackAndRunD((l) => { | ||||||
|       rasterLayerName = l.properties.name |       rasterLayerName = l.properties.name; | ||||||
|     }) |     }) | ||||||
|   ) |   ); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="absolute top-0 left-0 h-screen w-screen overflow-hidden"> | <div class="absolute top-0 left-0 h-screen w-screen overflow-hidden"> | ||||||
|  | @ -216,6 +223,15 @@ | ||||||
|       </div> |       </div> | ||||||
|     </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"> |     <div class="flex flex-col items-end"> | ||||||
|       <!-- bottom right elements --> |       <!-- bottom right elements --> | ||||||
|       <If condition={state.floors.map((f) => f.length > 1)}> |       <If condition={state.floors.map((f) => f.length > 1)}> | ||||||
|  | @ -247,15 +263,13 @@ | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| <LoginToggle ignoreLoading={true} {state}> | <LoginToggle ignoreLoading={true} {state}> | ||||||
|   <If condition={state.userRelatedState.showCrosshair.map((s) => s === "yes")}> |   {#if $showCrosshair === "yes" && ($currentZoom >= 17 || $arrowKeysWereUsed !== undefined) } | ||||||
|     <If condition={state.mapProperties.zoom.map((z) => z >= 17)}> |     <div | ||||||
|       <div |       class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center justify-center" | ||||||
|         class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center justify-center" |     > | ||||||
|       > |       <Cross class="h-4 w-4" /> | ||||||
|         <ToSvelte construct={Svg.cross_svg()} /> |     </div> | ||||||
|       </div> |   {/if} | ||||||
|     </If> |  | ||||||
|   </If> |  | ||||||
| </LoginToggle> | </LoginToggle> | ||||||
| 
 | 
 | ||||||
| <If | <If | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue