forked from MapComplete/MapComplete
		
	selected_element layer which highlights the selected element
This commit is contained in:
		
							parent
							
								
									e8ff43312f
								
							
						
					
					
						commit
						42bd301389
					
				
					 13 changed files with 146 additions and 32 deletions
				
			
		|  | @ -160,6 +160,11 @@ export default class FeaturePipeline { | |||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (id === "selected_element") { | ||||
|                 handlePriviligedFeatureSource(state.selectedElementsLayer) | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (id === "gps_location") { | ||||
|                 handlePriviligedFeatureSource(state.currentUserLocation) | ||||
|                 continue | ||||
|  |  | |||
|  | @ -1,29 +1,29 @@ | |||
| import UserRelatedState from "./UserRelatedState" | ||||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import {Store, Stores, UIEventSource} from "../UIEventSource" | ||||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import AvailableBaseLayers from "../Actors/AvailableBaseLayers" | ||||
| import Attribution from "../../UI/BigComponents/Attribution" | ||||
| import Minimap, { MinimapObj } from "../../UI/Base/Minimap" | ||||
| import { Tiles } from "../../Models/TileRange" | ||||
| import Minimap, {MinimapObj} from "../../UI/Base/Minimap" | ||||
| import {Tiles} from "../../Models/TileRange" | ||||
| import BaseUIElement from "../../UI/BaseUIElement" | ||||
| import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" | ||||
| import FilteredLayer, {FilterState} from "../../Models/FilteredLayer" | ||||
| import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" | ||||
| import { QueryParameters } from "../Web/QueryParameters" | ||||
| import {QueryParameters} from "../Web/QueryParameters" | ||||
| import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer" | ||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource" | ||||
| import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource" | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource" | ||||
| import {GeoOperations} from "../GeoOperations" | ||||
| import TitleHandler from "../Actors/TitleHandler" | ||||
| import { BBox } from "../BBox" | ||||
| import {BBox} from "../BBox" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import { TiledStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource" | ||||
| import { Translation, TypedTranslation } from "../../UI/i18n/Translation" | ||||
| import { Tag } from "../Tags/Tag" | ||||
| import { OsmConnection } from "../Osm/OsmConnection" | ||||
| import { Feature, GeoJSON, LineString } from "geojson" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
| import {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource" | ||||
| import {Translation, TypedTranslation} from "../../UI/i18n/Translation" | ||||
| import {Tag} from "../Tags/Tag" | ||||
| import {OsmConnection} from "../Osm/OsmConnection" | ||||
| import {Feature, LineString} from "geojson" | ||||
| import {OsmTags} from "../../Models/OsmFeature" | ||||
| 
 | ||||
| export interface GlobalFilter { | ||||
|     filter: FilterState | ||||
|  | @ -93,6 +93,13 @@ export default class MapState extends UserRelatedState { | |||
|      */ | ||||
|     public homeLocation: FeatureSourceForLayer & Tiled | ||||
| 
 | ||||
|     /** | ||||
|      * A builtin layer which contains the selected element. | ||||
|      * Loads 'selected_element.json' | ||||
|      * This _might_ contain multiple points, e.g. every center of a multipolygon | ||||
|      */ | ||||
|     public selectedElementsLayer: FeatureSourceForLayer & Tiled | ||||
| 
 | ||||
|     public readonly mainMapObject: BaseUIElement & MinimapObj | ||||
| 
 | ||||
|     /** | ||||
|  | @ -168,6 +175,7 @@ export default class MapState extends UserRelatedState { | |||
|         this.initGpsLocation() | ||||
|         this.initUserLocationTrail() | ||||
|         this.initCurrentView() | ||||
|         this.initSelectedElement() | ||||
| 
 | ||||
|         new TitleHandler(this) | ||||
|     } | ||||
|  | @ -249,7 +257,7 @@ export default class MapState extends UserRelatedState { | |||
| 
 | ||||
|     private initGpsLocation() { | ||||
|         // Initialize the gps layer data. This is emtpy for now, the actual writing happens in the Geolocationhandler
 | ||||
|         let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter( | ||||
|         const gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter( | ||||
|             (l) => l.layerDef.id === "gps_location" | ||||
|         )[0] | ||||
|         if (gpsLayerDef === undefined) { | ||||
|  | @ -258,6 +266,29 @@ export default class MapState extends UserRelatedState { | |||
|         this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0)) | ||||
|     } | ||||
| 
 | ||||
|     private initSelectedElement(){ | ||||
|         const layerDef: FilteredLayer = this.filteredLayers.data.filter( | ||||
|             (l) => l.layerDef.id === "selected_element" | ||||
|         )[0] | ||||
|         const empty = [] | ||||
|         const store = this.selectedElement.map(feature => { | ||||
|             if(feature === undefined || feature === null){ | ||||
|                 return empty | ||||
|             } | ||||
|             return [{ | ||||
|                 feature: { | ||||
|                     type:"Feature", | ||||
|                     properties: { | ||||
|                         selected: "yes", | ||||
|                         id: "selected" + feature.properties.id | ||||
|                     }, | ||||
|                     geometry:feature.geometry | ||||
|                 } | ||||
|                 , freshness: new Date()}]; | ||||
|         }); | ||||
|         this.selectedElementsLayer =  new TiledStaticFeatureSource(store,layerDef); | ||||
|     } | ||||
| 
 | ||||
|     private initUserLocationTrail() { | ||||
|         const features = LocalStorageSource.GetParsed<{ feature: any; freshness: Date }[]>( | ||||
|             "gps_location_history", | ||||
|  |  | |||
|  | @ -127,7 +127,8 @@ export default class MangroveReviews { | |||
|         this._lastUpdate = new Date() | ||||
| 
 | ||||
|         const self = this | ||||
|         mangrove.getReviews({ sub: this.GetSubjectUri() }).then((data) => { | ||||
|         mangrove.getReviews({ sub: this.GetSubjectUri() }) | ||||
|             .then((data) => { | ||||
|             const reviews = [] | ||||
|             const reviewsByUser = [] | ||||
|             for (const review of data.reviews) { | ||||
|  | @ -153,6 +154,9 @@ export default class MangroveReviews { | |||
|             } | ||||
|             self._reviews.setData(reviewsByUser.concat(reviews)) | ||||
|         }) | ||||
|             .catch(e => { | ||||
|                 console.error("Could not download review for ", e); | ||||
|             }) | ||||
|         return this._reviews | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,6 +27,7 @@ export default class Constants { | |||
|     ] | ||||
| 
 | ||||
|     public static readonly added_by_default: string[] = [ | ||||
|         "selected_element", | ||||
|         "gps_location", | ||||
|         "gps_location_history", | ||||
|         "home_location", | ||||
|  |  | |||
|  | @ -64,4 +64,15 @@ export default interface PointRenderingConfigJson { | |||
|      * Note that, if the wayhandling hides the icon then no label is shown as well. | ||||
|      */ | ||||
|     label?: string | TagRenderingConfigJson | ||||
| 
 | ||||
|     /** | ||||
|      * A snippet of css code | ||||
|      */ | ||||
|     css?: string | TagRenderingConfigJson | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * A snippet of css-classes. They can be space-separated | ||||
|      */ | ||||
|     cssClasses?: string | TagRenderingConfigJson | ||||
| } | ||||
|  |  | |||
|  | @ -301,7 +301,7 @@ export default class LayerConfig extends WithContextLoader { | |||
| 
 | ||||
|             const hasCenterRendering = this.mapRendering.some( | ||||
|                 (r) => | ||||
|                     r.location.has("centroid") || r.location.has("start") || r.location.has("end") | ||||
|                     r.location.has("centroid")  || r.location.has("projected_centerpoint") || r.location.has("start") || r.location.has("end") | ||||
|             ) | ||||
| 
 | ||||
|             if (this.lineRendering.length === 0 && this.mapRendering.length === 0) { | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ import { FixedUiElement } from "../../UI/Base/FixedUiElement" | |||
| import Img from "../../UI/Base/Img" | ||||
| import Combine from "../../UI/Base/Combine" | ||||
| import { VariableUiElement } from "../../UI/Base/VariableUIElement" | ||||
| import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson"; | ||||
| 
 | ||||
| export default class PointRenderingConfig extends WithContextLoader { | ||||
|     private static readonly allowed_location_codes = new Set<string>([ | ||||
|  | @ -25,11 +26,13 @@ export default class PointRenderingConfig extends WithContextLoader { | |||
|         "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string | ||||
|     > | ||||
| 
 | ||||
|     public readonly icon: TagRenderingConfig | ||||
|     public readonly icon?: TagRenderingConfig | ||||
|     public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[] | ||||
|     public readonly iconSize: TagRenderingConfig | ||||
|     public readonly label: TagRenderingConfig | ||||
|     public readonly rotation: TagRenderingConfig | ||||
|     public readonly cssDef: TagRenderingConfig | ||||
|     public readonly cssClasses?: TagRenderingConfig | ||||
| 
 | ||||
|     constructor(json: PointRenderingConfigJson, context: string) { | ||||
|         super(json, context) | ||||
|  | @ -61,6 +64,10 @@ export default class PointRenderingConfig extends WithContextLoader { | |||
|             ) | ||||
|         } | ||||
|         this.icon = this.tr("icon", undefined) | ||||
|         if(json.css !== undefined){ | ||||
|             this.cssDef = this.tr("css", undefined) | ||||
|         } | ||||
|         this.cssClasses = this.tr("cssClasses", undefined) | ||||
|         this.iconBadges = (json.iconBadges ?? []).map((overlay, i) => { | ||||
|             let tr: TagRenderingConfig | ||||
|             if ( | ||||
|  | @ -240,6 +247,9 @@ export default class PointRenderingConfig extends WithContextLoader { | |||
|             iconAndBadges.SetClass("w-full h-full") | ||||
|         } | ||||
| 
 | ||||
|         const css= this.cssDef?.GetRenderValue(tags , undefined)?.txt | ||||
|         const cssClasses = this.cssClasses?.GetRenderValue(tags , undefined)?.txt | ||||
| 
 | ||||
|         let label = this.GetLabel(tags) | ||||
|         let htmlEl: BaseUIElement | ||||
|         if (icon === undefined && label === undefined) { | ||||
|  | @ -252,6 +262,13 @@ export default class PointRenderingConfig extends WithContextLoader { | |||
|             htmlEl = new Combine([iconAndBadges, label]).SetStyle("flex flex-col") | ||||
|         } | ||||
| 
 | ||||
|         if(css !== undefined){ | ||||
|             htmlEl?.SetStyle(css) | ||||
|         } | ||||
| 
 | ||||
|         if(cssClasses !== undefined){ | ||||
|             htmlEl?.SetClass(cssClasses) | ||||
|         } | ||||
|         return { | ||||
|             html: htmlEl, | ||||
|             iconSize: [iconW, iconH], | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import Loc from "../../Models/Loc" | |||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" | ||||
| import * as L from "leaflet" | ||||
| import { Map } from "leaflet" | ||||
| import {LeafletMouseEvent, Map} from "leaflet" | ||||
| import Minimap, { MinimapObj, MinimapOptions } from "./Minimap" | ||||
| import { BBox } from "../../Logic/BBox" | ||||
| import "leaflet-polylineoffset" | ||||
|  | @ -316,8 +316,10 @@ export default class MinimapImplementation extends BaseUIElement implements Mini | |||
| 
 | ||||
|         if (this._options.lastClickLocation) { | ||||
|             const lastClickLocation = this._options.lastClickLocation | ||||
|             map.on("click", function (e) { | ||||
|                 // @ts-ignore
 | ||||
|             map.on("click", function (e: LeafletMouseEvent) { | ||||
|                 if(e.originalEvent["dismissed"] ){ | ||||
|                     return | ||||
|                 } | ||||
|                 lastClickLocation?.setData({ lat: e.latlng.lat, lon: e.latlng.lng }) | ||||
|             }) | ||||
| 
 | ||||
|  |  | |||
|  | @ -71,18 +71,22 @@ export default class ScrollableFullScreen { | |||
|                 self.Activate() | ||||
|             } else { | ||||
|                 // Some cleanup...
 | ||||
|                 ScrollableFullScreen.collapse() | ||||
| 
 | ||||
|                 const fs = document.getElementById("fullscreen") | ||||
|                 if (fs !== null) { | ||||
|                     ScrollableFullScreen.empty.AttachTo("fullscreen") | ||||
|                     fs.classList.add("hidden") | ||||
|                 } | ||||
| 
 | ||||
|                 ScrollableFullScreen._currentlyOpen?.isShown?.setData(false) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public static collapse(){ | ||||
|         const fs = document.getElementById("fullscreen") | ||||
|         if (fs !== null) { | ||||
|             ScrollableFullScreen.empty.AttachTo("fullscreen") | ||||
|             fs.classList.add("hidden") | ||||
|         } | ||||
| 
 | ||||
|         ScrollableFullScreen._currentlyOpen?.isShown?.setData(false) | ||||
|     } | ||||
| 
 | ||||
|     Destroy() { | ||||
|         this._fullscreencomponent.Destroy() | ||||
|     } | ||||
|  | @ -99,6 +103,7 @@ export default class ScrollableFullScreen { | |||
|         fs.classList.remove("hidden") | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private BuildComponent(title: BaseUIElement, content: BaseUIElement): BaseUIElement { | ||||
|         const returnToTheMap = new Combine([ | ||||
|             Svg.back_svg().SetClass("block md:hidden w-12 h-12 p-2 svg-foreground"), | ||||
|  |  | |||
|  | @ -123,6 +123,9 @@ export default class DefaultGUI { | |||
|                 addNewPoint, | ||||
|                 hasPresets ? new AddNewMarker(state.filteredLayers) : noteMarker | ||||
|             ) | ||||
|             state.LastClickLocation.addCallbackAndRunD(_ => { | ||||
|                 ScrollableFullScreen.collapse() | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         if (noteLayer !== undefined) { | ||||
|  | @ -153,6 +156,16 @@ export default class DefaultGUI { | |||
|             state, | ||||
|         }) | ||||
| 
 | ||||
|         const selectedElement: FilteredLayer = state.filteredLayers.data.filter( | ||||
|             (l) => l.layerDef.id === "selected_element" | ||||
|         )[0] | ||||
|         new ShowDataLayer({ | ||||
|             leafletMap: state.leafletMap, | ||||
|             layerToShow: selectedElement.layerDef, | ||||
|             features: state.selectedElementsLayer, | ||||
|             state | ||||
|         }) | ||||
| 
 | ||||
|         state.leafletMap.addCallbackAndRunD((_) => { | ||||
|             // Lets assume that all showDataLayers are initialized at this point
 | ||||
|             state.selectedElement.ping() | ||||
|  |  | |||
|  | @ -684,7 +684,6 @@ export default class TagRenderingQuestion extends Combine { | |||
|         const tagsData = tags.data | ||||
|         const feature = state?.allElements?.ContainingFeatures?.get(tagsData.id) | ||||
|         const center = feature != undefined ? GeoOperations.centerpointCoordinates(feature) : [0, 0] | ||||
|         console.log("Creating a tr-question with applicableUnit", applicableUnit) | ||||
|         const input: InputElement<string> = ValidatedTextField.ForType( | ||||
|             configuration.freeform.type | ||||
|         )?.ConstructInputElement({ | ||||
|  |  | |||
|  | @ -286,7 +286,6 @@ export default class ShowDataLayerImplementation { | |||
|         // Leaflet cannot handle geojson points natively
 | ||||
|         // We have to convert them to the appropriate icon
 | ||||
|         // Click handling is done in the next step
 | ||||
| 
 | ||||
|         const layer: LayerConfig = this._layerToShow | ||||
|         if (layer === undefined) { | ||||
|             return | ||||
|  | @ -337,7 +336,6 @@ export default class ShowDataLayerImplementation { | |||
|         const self = this | ||||
| 
 | ||||
|         function activate (event: LeafletMouseEvent) { | ||||
|             console.log("Activating!") | ||||
|             if (infobox === undefined) { | ||||
|                 const tags = | ||||
|                     self.allElements?.getEventSourceById(key) ?? | ||||
|  | @ -350,6 +348,14 @@ export default class ShowDataLayerImplementation { | |||
|                 }) | ||||
|             } | ||||
|             infobox.Activate() | ||||
|             self._selectedElement.setData( self.allElements.ContainingFeatures.get(feature.id) ?? feature ) | ||||
|             event?.originalEvent?.preventDefault() | ||||
|             event?.originalEvent?.stopPropagation() | ||||
|             event?.originalEvent?.stopImmediatePropagation() | ||||
|             if(event?.originalEvent){ | ||||
|                 // This is a total workaround, as 'preventDefault' and everything above seems to be not working
 | ||||
|                 event.originalEvent["dismissed"] = true | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         leafletLayer.addEventListener('click', activate) | ||||
|  |  | |||
							
								
								
									
										20
									
								
								assets/layers/selected_element/selected_element.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								assets/layers/selected_element/selected_element.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| { | ||||
|   "id": "selected_element", | ||||
|   "description": { | ||||
|     "en": "Highlights the currently selected element. Override this layer to have different colors", | ||||
|     "nl": "Toont het geselecteerde element" | ||||
|   }, | ||||
|   "source": { | ||||
|     "osmTags": "selected=yes", | ||||
|     "maxCacheAge": 0 | ||||
|   }, | ||||
|   "mapRendering": [ | ||||
|     { | ||||
|       "icon": "circle:red", | ||||
|       "iconSize": "1,1,center", | ||||
|       "location": ["point","projected_centerpoint"], | ||||
|       "css": "box-shadow: red 0 0 20px 20px; z-index: -1; height: 1px; width: 1px;", | ||||
|       "cssClasses": "block relative rounded-full" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue