forked from MapComplete/MapComplete
		
	Add search previews on the map
This commit is contained in:
		
							parent
							
								
									1c46a65c84
								
							
						
					
					
						commit
						4f52483a98
					
				
					 19 changed files with 315 additions and 87 deletions
				
			
		|  | @ -356,9 +356,32 @@ | |||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "#": "ignore-image-in-then", | ||||
|           "if": "osm_id~*", | ||||
|           "then": { | ||||
|             "special": { | ||||
|               "type": "link", | ||||
|               "text": "<img alt='on osm' textmode='🗺️' src='./assets/svg/osm-logo-us.svg'/>", | ||||
|               "href": "https://www.openstreetmap.org/{osm_id}", | ||||
|               "arialabel": { | ||||
|                 "en": "Open on openstreetmap.org", | ||||
|                 "nl": "Bekijk op openstreetmap.org", | ||||
|                 "de": "Auf openstreetmap.org öffnen", | ||||
|                 "pl": "Otwórz na openstreetmap.org", | ||||
|                 "da": "Åbn på openstreetmap.org" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       ], | ||||
|       "condition": "id~(node|way|relation)/[0-9]*" | ||||
|       "condition": { | ||||
|         "or": [ | ||||
|           "id~(node|way|relation)/[0-9]*", | ||||
|           "osm_id~*" | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "id": "rating", | ||||
|  |  | |||
							
								
								
									
										66
									
								
								assets/layers/search/search.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								assets/layers/search/search.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,66 @@ | |||
| { | ||||
|   "id": "search", | ||||
|   "description": { | ||||
|     "en": "Priviliged layer showing the search results" | ||||
|   }, | ||||
|   "source": "special", | ||||
|   "title": "{display_name}", | ||||
|   "tagRenderings": [ | ||||
|     { | ||||
|       "id": "intro", | ||||
|       "render": { | ||||
|         "en": "Search result" | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "id": "osm", | ||||
|       "render": { | ||||
|         "*": "<a href='https://openstreetmap.org/{osm_type}/{osm_id}'>On OpenStreetMap</a>" | ||||
|       } | ||||
|     }, | ||||
|     "all_tags" | ||||
|   ], | ||||
|   "pointRendering": [ | ||||
|     { | ||||
|       "location": [ | ||||
|         "point", | ||||
|         "centroid" | ||||
|       ], | ||||
|       "marker": [ | ||||
|         { | ||||
|           "icon": "circle", | ||||
|           "color": "white" | ||||
|         }, | ||||
|         { | ||||
|           "icon": { | ||||
|             "render": "globe_alt", | ||||
|             "mappings": [ | ||||
|               { | ||||
|                 "if": "category~city|locality|county", | ||||
|                 "then": "building_office_2" | ||||
|               }, | ||||
|               { | ||||
|                 "if": "category=train_station", | ||||
|                 "then": "train" | ||||
|               }, | ||||
|               { | ||||
|                 "if": "category=airport", | ||||
|                 "then": "airport" | ||||
|               }, | ||||
|               { | ||||
|                 "if": "category=house", | ||||
|                 "then": "house" | ||||
|               }, | ||||
|               { | ||||
|                 "if": "category=shop", | ||||
|                 "then": "building_storefront" | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         } | ||||
|       ], | ||||
|       "label": "{display_name}", | ||||
|       "labelCssClasses": "bg-white rounded p-2 no-wrap" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -89,7 +89,7 @@ | |||
|     "generate:contributor-list": "vite-node scripts/generateContributors.ts", | ||||
|     "generate:service-worker": "tsc src/service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i.bak \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js && rm public/service-worker.js.bak", | ||||
|     "reset:layeroverview": "npm run prep:layeroverview && npm run generate:layeroverview && npm run refresh:layeroverview", | ||||
|     "prep:layeroverview": "mkdir -p ./src/assets/generated/layers; echo {\\\"themes\\\":[]} > ./src/assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./src/assets/generated/known_layers.json && rm -f ./src/assets/generated/layers/*.json && rm -f ./src/assets/generated/themes/*.json && cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json && echo '{}' > ./src/assets/generated/layers/favourite.json && echo '{}' > ./src/assets/generated/layers/summary.json && echo '{}' > ./src/assets/generated/layers/last_click.json && echo '[]' > ./src/assets/generated/theme_overview.json", | ||||
|     "prep:layeroverview": "mkdir -p ./src/assets/generated/layers; echo {\\\"themes\\\":[]} > ./src/assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./src/assets/generated/known_layers.json && rm -f ./src/assets/generated/layers/*.json && rm -f ./src/assets/generated/themes/*.json && cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json && echo '{}' > ./src/assets/generated/layers/favourite.json && echo '{}' > ./src/assets/generated/layers/summary.json && echo '{}' > ./src/assets/generated/layers/last_click.json && echo '[]' > ./src/assets/generated/theme_overview.json && echo '{}' > ./src/assets/generated/layers/search.json", | ||||
|     "generate": "npm run generate:licenses && npm run generate:images && npm run generate:charging-stations && npm run generate:translations && npm run refresh:layeroverview && npm run generate:service-worker", | ||||
|     "generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -", | ||||
|     "clean:tests": "find . -type f -name \"*.doctest.ts\" | xargs -r rm", | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ export default class TitleHandler { | |||
|                 if (selected === undefined) { | ||||
|                     return defaultTitle | ||||
|                 } | ||||
|                 const layer = state.layout.getMatchingLayer(selected.properties) | ||||
|                 const layer = state.getMatchingLayer(selected.properties) | ||||
|                 if (layer === undefined) { | ||||
|                     return defaultTitle | ||||
|                 } | ||||
|  |  | |||
							
								
								
									
										41
									
								
								src/Logic/Geocoding/GeocodingFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/Logic/Geocoding/GeocodingFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | |||
| import { GeoCodeResult } from "./GeocodingProvider" | ||||
| import { Store } from "../UIEventSource" | ||||
| import { FeatureSource } from "../FeatureSource/FeatureSource" | ||||
| import { Feature, Geometry } from "geojson" | ||||
| 
 | ||||
| export default class GeocodingFeatureSource implements FeatureSource { | ||||
|     public features: Store<Feature<Geometry, Record<string, string>>[]> | ||||
| 
 | ||||
|     constructor(provider: Store<GeoCodeResult[]>) { | ||||
|         this.features = provider.mapD(geocoded => { | ||||
|             const features: Feature[] = [] | ||||
| 
 | ||||
|             for (const gc of geocoded) { | ||||
|                 if (gc.lat === undefined || gc.lon === undefined) { | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 features.push({ | ||||
|                     type: "Feature", | ||||
|                     properties: { | ||||
|                         id: "search_result_" + gc.osm_type + "/" + gc.osm_id, | ||||
|                         category: gc.category, | ||||
|                         description: gc.description, | ||||
|                         display_name: gc.display_name, | ||||
|                         osm_id: gc.osm_type + "/" + gc.osm_id, | ||||
|                         osm_key: gc.feature?.properties?.osm_key, | ||||
|                         osm_value: gc.feature?.properties?.osm_value | ||||
|                     }, | ||||
|                     geometry: { | ||||
|                         type: "Point", | ||||
|                         coordinates: [gc.lon, gc.lat] | ||||
|                     } | ||||
|                 }) | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             return features | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -2,8 +2,10 @@ import { BBox } from "../BBox" | |||
| import { Feature, Geometry } from "geojson" | ||||
| import { DefaultPinIcon } from "../../Models/Constants" | ||||
| import { Store } from "../UIEventSource" | ||||
| 
 | ||||
| export type GeocodingCategory = "coordinate" | "city" | "house" | "street" | "locality" | "country" | "train_station" | "county" | "airport" | ||||
| import * as search from "../../assets/generated/layers/search.json" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" | ||||
| export type GeocodingCategory = "coordinate" | "city" | "house" | "street" | "locality" | "country" | "train_station" | "county" | "airport" | "shop" | ||||
| 
 | ||||
| export type GeoCodeResult = { | ||||
|     /** | ||||
|  | @ -66,6 +68,8 @@ export interface ReverseGeocodingProvider { | |||
| 
 | ||||
| export class GeocodingUtils { | ||||
| 
 | ||||
|     public static searchLayer=  new LayerConfig(<LayerConfigJson> search, "search") | ||||
| 
 | ||||
|     public static categoryToZoomLevel: Record<GeocodingCategory, number> = { | ||||
|         city: 12, | ||||
|         county: 10, | ||||
|  | @ -75,7 +79,8 @@ export class GeocodingUtils { | |||
|         locality: 14, | ||||
|         street: 15, | ||||
|         train_station: 14, | ||||
|         airport: 13 | ||||
|         airport: 13, | ||||
|         shop:16 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  | @ -89,7 +94,8 @@ export class GeocodingUtils { | |||
|         street: "globe_alt", | ||||
|         train_station: "train", | ||||
|         county: "building_office_2", | ||||
|         airport: "airport" | ||||
|         airport: "airport", | ||||
|         shop: "building_storefront" | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -95,6 +95,9 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | |||
| 
 | ||||
|     private getCategory(entry: Feature) { | ||||
|         const p = entry.properties | ||||
|         if(p.osm_key === "shop"){ | ||||
|             return "shop" | ||||
|         } | ||||
|         if (p.osm_value === "train_station" || p.osm_key === "railway") { | ||||
|             return "train_station" | ||||
|         } | ||||
|  |  | |||
|  | @ -12,10 +12,41 @@ export class RecentSearch { | |||
|     public readonly seenThisSession: Store<GeoCodeResult[]> | ||||
| 
 | ||||
|     constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) { | ||||
|      //   const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches")
 | ||||
|         const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches") | ||||
|         prefs.addCallbackAndRunD(prev => console.trace("Previous searches are:", prev)) | ||||
|         prefs.set(null) | ||||
|         this._seenThisSession =  new UIEventSource<GeoCodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
 | ||||
|         this.seenThisSession = this._seenThisSession | ||||
| 
 | ||||
|         prefs.addCallbackAndRunD(prefs => { | ||||
|             if(prefs === ""){ | ||||
|                 return | ||||
|             } | ||||
|             const simpleArr = <GeoCodeResult[]> JSON.parse(prefs) | ||||
|             if(simpleArr.length > 0){ | ||||
|                 this._seenThisSession.set(simpleArr) | ||||
|                 return true | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         this.seenThisSession.stabilized(2500).addCallbackAndRunD(seen => { | ||||
|             const results=  [] | ||||
|             for (let i = 0; i < Math.min(3, seen.length); i++) { | ||||
|                 const gc = seen[i] | ||||
|                 const simple = { | ||||
|                     category: gc.category, | ||||
|                     description: gc.description, | ||||
|                     display_name: gc.display_name, | ||||
|                     lat: gc.lat, lon: gc.lon, | ||||
|                     osm_id: gc.osm_id, | ||||
|                     osm_type: gc.osm_type | ||||
|                 } | ||||
|                 results.push(simple) | ||||
|             } | ||||
|             console.log("Setting", results) | ||||
|             prefs.setData(JSON.stringify(results)) | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
|         state.selectedElement.addCallbackAndRunD(selected => { | ||||
| 
 | ||||
|  | @ -23,6 +54,10 @@ export class RecentSearch { | |||
|             if(!osm_id){ | ||||
|                 return | ||||
|             } | ||||
|             console.log("Selected element is", selected) | ||||
|             if(["node","way","relation"].indexOf(osm_type) < 0){ | ||||
|                 return | ||||
|             } | ||||
|             const [lon, lat] = GeoOperations.centerpointCoordinates(selected) | ||||
|             const entry = <GeoCodeResult>{ | ||||
|                 feature: selected, | ||||
|  | @ -46,6 +81,7 @@ export class RecentSearch { | |||
|                 seenIds.add(id) | ||||
|             } | ||||
|         } | ||||
|         console.log(">>>",arr) | ||||
|         this._seenThisSession.set(arr) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,9 +2,7 @@ import { UIEventSource } from "../UIEventSource" | |||
| import UserDetails, { OsmConnection } from "./OsmConnection" | ||||
| import { Utils } from "../../Utils" | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||
| // @ts-ignore
 | ||||
| import { osmAuth } from "osm-auth" | ||||
| import OSMAuthInstance = OSMAuth.OSMAuthInstance | ||||
| import OSMAuthInstance = OSMAuth.osmAuth | ||||
| 
 | ||||
| export class OsmPreferences { | ||||
|     /** | ||||
|  | @ -53,7 +51,7 @@ export class OsmPreferences { | |||
|         const subOptions = { prefix: "" } | ||||
|         // Gives the number of combined preferences
 | ||||
|         const length = this.GetPreference(allStartWith + "-length", "", subOptions) | ||||
| 
 | ||||
|         const preferences = this.preferences | ||||
|         if ((allStartWith + "-length").length > 255) { | ||||
|             throw ( | ||||
|                 "This preference key is too long, it has " + | ||||
|  | @ -64,7 +62,6 @@ export class OsmPreferences { | |||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         const self = this | ||||
|         source.addCallback((str) => { | ||||
|             if (str === undefined || str === "") { | ||||
|                 return | ||||
|  | @ -74,9 +71,9 @@ export class OsmPreferences { | |||
|                 const count = parseInt(length.data) | ||||
|                 for (let i = 0; i < count; i++) { | ||||
|                     // Delete all the preferences
 | ||||
|                     self.GetPreference(allStartWith + "-" + i, "", subOptions).setData("") | ||||
|                     this.GetPreference(allStartWith + "-" + i, "", subOptions).setData("") | ||||
|                 } | ||||
|                 self.GetPreference(allStartWith + "-length", "", subOptions).setData("") | ||||
|                 this.GetPreference(allStartWith + "-length", "", subOptions).setData("") | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|  | @ -99,7 +96,7 @@ export class OsmPreferences { | |||
|                 if (i > 100) { | ||||
|                     throw "This long preference is getting very long... " | ||||
|                 } | ||||
|                 self.GetPreference(allStartWith + "-" + i, "", subOptions).setData( | ||||
|                 this.GetPreference(allStartWith + "-" + i, "", subOptions).setData( | ||||
|                     str.substr(0, 255) | ||||
|                 ) | ||||
|                 str = str.substr(255) | ||||
|  | @ -108,8 +105,9 @@ export class OsmPreferences { | |||
|             length.setData("" + i) // We use I, the number of preference fields used
 | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         function updateData(l: number) { | ||||
|             if (Object.keys(self.preferences.data).length === 0) { | ||||
|             if (Object.keys(preferences.data).length === 0) { | ||||
|                 // The preferences are still empty - they are not yet updated, so we delay updating for now
 | ||||
|                 return | ||||
|             } | ||||
|  | @ -120,15 +118,21 @@ export class OsmPreferences { | |||
|             let str = "" | ||||
|             for (let i = 0; i < prefsCount; i++) { | ||||
|                 const key = allStartWith + "-" + i | ||||
|                 if (self.preferences.data[key] === undefined) { | ||||
|                 if (preferences.data[key] === undefined) { | ||||
|                     console.warn( | ||||
|                         "Detected a broken combined preference:", | ||||
|                         key, | ||||
|                         "is undefined", | ||||
|                         self.preferences | ||||
|                         preferences | ||||
|                     ) | ||||
|                     continue | ||||
|                 } | ||||
|                 str += self.preferences.data[key] ?? "" | ||||
|                 const v = preferences.data[key] | ||||
|                 if(v === "undefined"){ | ||||
|                     delete preferences.data[key] | ||||
|                     continue | ||||
|                 } | ||||
|                 str += preferences.data[key] ?? "" | ||||
|             } | ||||
| 
 | ||||
|             source.setData(str) | ||||
|  | @ -137,7 +141,7 @@ export class OsmPreferences { | |||
|         length.addCallback((l) => { | ||||
|             updateData(Number(l)) | ||||
|         }) | ||||
|         this.preferences.addCallbackAndRun((_) => { | ||||
|         this.preferences.addCallbackAndRun(() => { | ||||
|             updateData(Number(length.data)) | ||||
|         }) | ||||
| 
 | ||||
|  | @ -159,7 +163,7 @@ export class OsmPreferences { | |||
|             ) | ||||
|         } | ||||
|         key = prefix + key | ||||
|         key = key.replace(/[:\\\/"' {}.%]/g, "") | ||||
|         key = key.replace(/[:/"' {}.%\\]/g, "") | ||||
|         if (key.length >= 255) { | ||||
|             throw "Preferences: key length to big" | ||||
|         } | ||||
|  | @ -193,7 +197,6 @@ export class OsmPreferences { | |||
| 
 | ||||
|     public ClearPreferences() { | ||||
|         let isRunning = false | ||||
|         const self = this | ||||
|         this.preferences.addCallback((prefs) => { | ||||
|             console.log("Cleaning preferences...") | ||||
|             if (Object.keys(prefs).length == 0) { | ||||
|  | @ -208,7 +211,7 @@ export class OsmPreferences { | |||
|                 const matches = prefixes.some((prefix) => key.startsWith(prefix)) | ||||
|                 if (matches) { | ||||
|                     console.log("Clearing ", key) | ||||
|                     self.GetPreference(key, "", { prefix: "" }).setData("") | ||||
|                     this.GetPreference(key, "", { prefix: "" }).setData("") | ||||
|                 } | ||||
|             } | ||||
|             isRunning = false | ||||
|  | @ -227,7 +230,6 @@ export class OsmPreferences { | |||
|     } | ||||
| 
 | ||||
|     private UpdatePreferences(forceUpdate?: boolean) { | ||||
|         const self = this | ||||
|         if (this._fakeUser) { | ||||
|             return | ||||
|         } | ||||
|  | @ -236,7 +238,7 @@ export class OsmPreferences { | |||
|                 method: "GET", | ||||
|                 path: "/api/0.6/user/preferences", | ||||
|             }, | ||||
|             function (error, value: XMLDocument) { | ||||
|              (error, value: XMLDocument) => { | ||||
|                 if (error) { | ||||
|                     console.log("Could not load preferences", error) | ||||
|                     return | ||||
|  | @ -246,34 +248,33 @@ export class OsmPreferences { | |||
|                 for (let i = 0; i < prefs.length; i++) { | ||||
|                     const pref = prefs[i] | ||||
|                     const k = pref.getAttribute("k") | ||||
|                     const v = pref.getAttribute("v") | ||||
|                     self.preferences.data[k] = v | ||||
|                     this.preferences.data[k] = pref.getAttribute("v") | ||||
|                     seenKeys.add(k) | ||||
|                 } | ||||
|                 if (forceUpdate) { | ||||
|                     for (let key in self.preferences.data) { | ||||
|                     for (const key in this.preferences.data) { | ||||
|                         if (seenKeys.has(key)) { | ||||
|                             continue | ||||
|                         } | ||||
|                         console.log("Deleting key", key, "as we didn't find it upstream") | ||||
|                         delete self.preferences.data[key] | ||||
|                         delete this.preferences.data[key] | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 // We merge all the preferences: new keys are uploaded
 | ||||
|                 // For differing values, the server overrides local changes
 | ||||
|                 self.preferenceSources.forEach((preference, key) => { | ||||
|                     const osmValue = self.preferences.data[key] | ||||
|                 this.preferenceSources.forEach((preference, key) => { | ||||
|                     const osmValue = this.preferences.data[key] | ||||
|                     if (osmValue === undefined && preference.data !== undefined) { | ||||
|                         // OSM doesn't know this value yet
 | ||||
|                         self.UploadPreference(key, preference.data) | ||||
|                         this.UploadPreference(key, preference.data) | ||||
|                     } else { | ||||
|                         // OSM does have a value - set it
 | ||||
|                         preference.setData(osmValue) | ||||
|                     } | ||||
|                 }) | ||||
| 
 | ||||
|                 self.preferences.ping() | ||||
|                 this.preferences.ping() | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|  | @ -287,7 +288,6 @@ export class OsmPreferences { | |||
|         if (this.preferences.data[k] === v) { | ||||
|             return | ||||
|         } | ||||
|         const self = this | ||||
|         console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15)) | ||||
|         if (this._fakeUser) { | ||||
|             return | ||||
|  | @ -299,13 +299,13 @@ export class OsmPreferences { | |||
|                     path: "/api/0.6/user/preferences/" + encodeURIComponent(k), | ||||
|                     headers: { "Content-Type": "text/plain" }, | ||||
|                 }, | ||||
|                 function (error) { | ||||
|                 (error) => { | ||||
|                     if (error) { | ||||
|                         console.warn("Could not remove preference", error) | ||||
|                         return | ||||
|                     } | ||||
|                     delete self.preferences.data[k] | ||||
|                     self.preferences.ping() | ||||
|                     delete this.preferences.data[k] | ||||
|                     this.preferences.ping() | ||||
|                     console.debug("Preference ", k, "removed!") | ||||
|                 } | ||||
|             ) | ||||
|  | @ -319,13 +319,13 @@ export class OsmPreferences { | |||
|                 headers: { "Content-Type": "text/plain" }, | ||||
|                 content: v, | ||||
|             }, | ||||
|             function (error) { | ||||
|             (error)=>  { | ||||
|                 if (error) { | ||||
|                     console.warn(`Could not set preference "${k}"'`, error) | ||||
|                     return | ||||
|                 } | ||||
|                 self.preferences.data[k] = v | ||||
|                 self.preferences.ping() | ||||
|                 this.preferences.data[k] = v | ||||
|                 this.preferences.ping() | ||||
|                 console.debug(`Preference ${k} written!`) | ||||
|             } | ||||
|         ) | ||||
|  |  | |||
|  | @ -351,8 +351,10 @@ export default class UserRelatedState { | |||
|                     const key = k.substring(0, k.length - "length".length) | ||||
|                     let combined = "" | ||||
|                     for (let i = 0; i < l; i++) { | ||||
|                         combined += newPrefs[key + i] | ||||
|                         console.log("Building preference:",key,i,">>>", newPrefs[key + i], "<<<", newPrefs, ) | ||||
|                         combined += (newPrefs[key + i]) | ||||
|                     } | ||||
|                     console.log("Combined",key,">>>",combined) | ||||
|                     amendedPrefs.data[key.substring(0, key.length - "-combined-".length)] = combined | ||||
|                 } else { | ||||
|                     amendedPrefs.data[k] = newPrefs[k] | ||||
|  | @ -456,11 +458,15 @@ export default class UserRelatedState { | |||
|         amendedPrefs.addCallbackD((tags) => { | ||||
|             for (const key in tags) { | ||||
|                 if (key.startsWith("_") || key === "mapcomplete-language") { | ||||
|                     // Language is managed seperately
 | ||||
|                     // Language is managed separately
 | ||||
|                     continue | ||||
|                 } | ||||
|                 if (tags[key + "-combined-0"]) { | ||||
|                     // A combined value exists
 | ||||
|                     if(tags[key].startsWith("undefined")){ | ||||
|                         // Sometimes, a long string of 'undefined' will show up, we ignore them
 | ||||
|                         continue | ||||
|                     } | ||||
|                     this.osmConnection.GetLongPreference(key, "").setData(tags[key]) | ||||
|                 } else { | ||||
|                     this.osmConnection | ||||
|  |  | |||
|  | @ -91,6 +91,20 @@ export class Stores { | |||
|         }) | ||||
|         return stable | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * Constructs a new store, but tries to keep the value 'defined' | ||||
|      * If a defined value was in the stream once, a defined value will be returned | ||||
|      * @param store | ||||
|      */ | ||||
|     static holdDefined<T>(store: Store<T | undefined>): Store<T | undefined> { | ||||
|         const newStore = new UIEventSource(store.data) | ||||
|         store.addCallbackD(t => { | ||||
|             newStore.setData(t) | ||||
|         }) | ||||
|         return newStore | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export abstract class Store<T> implements Readable<T> { | ||||
|  |  | |||
|  | @ -26,6 +26,7 @@ export default class Constants { | |||
|         "last_click", | ||||
|         "favourite", | ||||
|         "summary", | ||||
|         "search" | ||||
|     ] as const | ||||
|     /** | ||||
|      * Special layers which are not included in a theme by default | ||||
|  | @ -38,7 +39,7 @@ export default class Constants { | |||
|         "import_candidate", | ||||
|         "usersettings", | ||||
|         "icons", | ||||
|         "filters", | ||||
|         "filters" | ||||
|     ] as const | ||||
|     /** | ||||
|      * Layer IDs of layers which have special properties through built-in hooks | ||||
|  | @ -126,6 +127,7 @@ export default class Constants { | |||
|         "brick_wall_round", | ||||
|         "brick_wall_square", | ||||
|         "building_office_2", | ||||
|         "building_storefront", | ||||
|         "bug", | ||||
|         "checkmark", | ||||
|         "checkmark", | ||||
|  |  | |||
|  | @ -340,7 +340,7 @@ export default class LayoutConfig implements LayoutInformation { | |||
|                 } | ||||
|             } | ||||
|         } | ||||
|         console.log("Fallthrough", this, tags) | ||||
|         console.trace("Fallthrough: could not find the appropraite layer for an object with tags", tags, "within layout", this) | ||||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|  | @ -354,7 +354,7 @@ export default class LayoutConfig implements LayoutInformation { | |||
|             ...json, | ||||
|             layers: json.layers.filter((l) => l["id"] !== "favourite"), | ||||
|         } | ||||
|         const usedImages = json._usedImages | ||||
|         const usedImages = jsonNoFavourites._usedImages | ||||
|         usedImages.sort() | ||||
| 
 | ||||
|         this.usedImages = Utils.Dedup(usedImages) | ||||
|  |  | |||
|  | @ -68,7 +68,7 @@ import Locale from "../UI/i18n/Locale" | |||
| import Hash from "../Logic/Web/Hash" | ||||
| import { GeoOperations } from "../Logic/GeoOperations" | ||||
| import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" | ||||
| import GeocodingProvider from "../Logic/Geocoding/GeocodingProvider" | ||||
| import GeocodingProvider, { GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider" | ||||
| import CombinedSearcher from "../Logic/Geocoding/CombinedSearcher" | ||||
| import CoordinateSearch from "../Logic/Geocoding/CoordinateSearch" | ||||
| import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch" | ||||
|  | @ -774,6 +774,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|             favourite: this.favourites, | ||||
|             summary: this.featureSummary, | ||||
|             last_click: this.lastClickObject, | ||||
|             search: undefined | ||||
|         } | ||||
| 
 | ||||
|         this.closestFeatures.registerSource(specialLayers.favourite, "favourite") | ||||
|  | @ -910,6 +911,34 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         this.selectedElement.setData(this.currentView.features?.data?.[0]) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the layout | ||||
|      * @param tags | ||||
|      */ | ||||
|     public getMatchingLayer(properties: Record<string, string>){ | ||||
| 
 | ||||
|         const id = properties.id | ||||
| 
 | ||||
|         if (id.startsWith("summary_")) { | ||||
|             // We don't select 'summary'-objects
 | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         if (id === "settings") { | ||||
|             return UserRelatedState.usersettingsConfig | ||||
|         } | ||||
|         if (id.startsWith(LastClickFeatureSource.newPointElementId)) { | ||||
|             return this.layout.layers.find((l) => l.id === "last_click") | ||||
|         } | ||||
|         if (id.startsWith("search_result")) { | ||||
|             return GeocodingUtils.searchLayer | ||||
|         } | ||||
|         if (id === "location_track") { | ||||
|             return this.layout.layers.find((l) => l.id === "gps_track") | ||||
|         } | ||||
|         return this.layout.getMatchingLayer(properties) | ||||
|     } | ||||
| 
 | ||||
|     public async reportError(message: string | Error | XMLHttpRequest) { | ||||
|         const isTesting = this.featureSwitchIsTesting.data | ||||
|         console.log( | ||||
|  |  | |||
|  | @ -7,23 +7,17 @@ | |||
|   import { LastClickFeatureSource } from "../../Logic/FeatureSource/Sources/LastClickFeatureSource" | ||||
|   import Loading from "./Loading.svelte" | ||||
|   import { onDestroy } from "svelte" | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
|   import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider" | ||||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
| 
 | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let selected: Feature | ||||
|   let tags = state.featureProperties.getStore(selected.properties.id) | ||||
| 
 | ||||
|   export let absolute = true | ||||
|   function getLayer(properties: Record<string, string>) { | ||||
|     if (properties.id === "settings") { | ||||
|       return UserRelatedState.usersettingsConfig | ||||
|     } | ||||
|     if (properties.id.startsWith(LastClickFeatureSource.newPointElementId)) { | ||||
|       return state.layout.layers.find((l) => l.id === "last_click") | ||||
|     } | ||||
|     if (properties.id === "location_track") { | ||||
|       return state.layout.layers.find((l) => l.id === "gps_track") | ||||
|     } | ||||
|     return state.layout.getMatchingLayer(properties) | ||||
|   function getLayer(properties: Record<string, string>): LayerConfig { | ||||
|     return state.getMatchingLayer(properties) | ||||
|   } | ||||
| 
 | ||||
|   let layer = getLayer(selected.properties) | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script lang="ts"> | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { Feature } from "geojson" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
|  | @ -17,10 +17,14 @@ | |||
|   import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider" | ||||
| 
 | ||||
|   import SearchResults from "./SearchResults.svelte" | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||
|   import MoreScreen from "./MoreScreen" | ||||
|   import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||
|   import { focusWithArrows } from "../../Utils/focusWithArrows" | ||||
|   import ShowDataLayer from "../Map/ShowDataLayer" | ||||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
|   import GeocodingFeatureSource from "../../Logic/Geocoding/GeocodingFeatureSource" | ||||
|   import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson.js" | ||||
| 
 | ||||
|   export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined | ||||
|   export let bounds: UIEventSource<BBox> | ||||
|  | @ -29,11 +33,11 @@ | |||
|   export let geolocationState: GeoLocationState | undefined = undefined | ||||
|   export let clearAfterView: boolean = true | ||||
|   export let searcher: GeocodingProvider = new NominatimGeocoding() | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let state: ThemeViewState | ||||
|   let searchContents: UIEventSource<string> = new UIEventSource<string>("") | ||||
|   export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined) | ||||
|   onDestroy( | ||||
|     triggerSearch.addCallback((_) => { | ||||
|     triggerSearch.addCallback(() => { | ||||
|       performSearch() | ||||
|     }) | ||||
|   ) | ||||
|  | @ -139,7 +143,18 @@ | |||
|       if (search.length === 0) { | ||||
|         return undefined | ||||
|       } | ||||
|       return searcher.suggest(search, { bbox: bounds.data }) | ||||
|       return Stores.holdDefined(bounds.bindD(bbox => searcher.suggest(search, { bbox, limit: 15 }))) | ||||
|     } | ||||
|   ) | ||||
|   let geocededFeatures=  new GeocodingFeatureSource(suggestions.stabilized(250)) | ||||
|   state.featureProperties.trackFeatureSource(geocededFeatures) | ||||
| 
 | ||||
|   new ShowDataLayer( | ||||
|     state.map, | ||||
|     { | ||||
|       layer: GeocodingUtils.searchLayer, | ||||
|       features:  geocededFeatures, | ||||
|       selectedElement: state.selectedElement | ||||
|     } | ||||
|   ) | ||||
| 
 | ||||
|  | @ -147,19 +162,18 @@ | |||
| 
 | ||||
|   function checkFocus() { | ||||
|     window.requestAnimationFrame(() => { | ||||
|       if (geosearch.contains(document.activeElement)) { | ||||
|       if (geosearch?.contains(document.activeElement)) { | ||||
|         return | ||||
|       } | ||||
|       isFocused.setData(false) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   document.addEventListener("focus",() => { | ||||
|   document.addEventListener("focus", () => { | ||||
|     checkFocus() | ||||
|   }, true /* use 'capturing' instead of bubbling, needed for focus-events*/) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div bind:this={geosearch} use:focusWithArrows={"searchresult"}> | ||||
|  |  | |||
|  | @ -42,6 +42,7 @@ | |||
|   import BuildingOffice2 from "@babeard/svelte-heroicons/outline/BuildingOffice2" | ||||
|   import Train from "../../assets/svg/Train.svelte" | ||||
|   import Airport from "../../assets/svg/Airport.svelte" | ||||
|   import BuildingStorefront from "@babeard/svelte-heroicons/outline/BuildingStorefront" | ||||
| 
 | ||||
|   /** | ||||
|    * Renders a single icon. | ||||
|  | @ -159,6 +160,8 @@ | |||
|     <Train {color} class={clss}/> | ||||
|   {:else if icon === "airport"} | ||||
|     <Airport {color} class={clss}/> | ||||
|   {:else if icon === "building_storefront"} | ||||
|     <BuildingStorefront {color} class={clss}/> | ||||
|   {:else if Utils.isEmoji(icon)} | ||||
|     <span style={`font-size: ${emojiHeight}px; line-height: ${emojiHeight}px`}> | ||||
|       {icon} | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import LayoutConfig, { MinimalLayoutInformation } from "../Models/ThemeConfig/La | |||
| import { | ||||
|     FeatureSource, | ||||
|     IndexedFeatureSource, | ||||
|     WritableFeatureSource, | ||||
|     WritableFeatureSource | ||||
| } from "../Logic/FeatureSource/FeatureSource" | ||||
| import { OsmConnection } from "../Logic/Osm/OsmConnection" | ||||
| import { Changes } from "../Logic/Osm/Changes" | ||||
|  | @ -97,8 +97,10 @@ export interface SpecialVisualizationState { | |||
|     readonly geolocation: GeoLocationHandler | ||||
|     readonly recentlySearched: RecentSearch | ||||
| 
 | ||||
|     getMatchingLayer(properties: Record<string, string>); | ||||
| 
 | ||||
|     showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer | ||||
| 
 | ||||
|     reportError(message: string): Promise<void> | ||||
| } | ||||
| 
 | ||||
|  | @ -134,7 +136,7 @@ export interface SpecialVisualization { | |||
| export type RenderingSpecification = | ||||
|     | string | ||||
|     | { | ||||
|           func: SpecialVisualization | ||||
|           args: string[] | ||||
|           style: string | ||||
|       } | ||||
|     func: SpecialVisualization | ||||
|     args: string[] | ||||
|     style: string | ||||
| } | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ | |||
|     EyeIcon, | ||||
|     HeartIcon, | ||||
|     MenuIcon, | ||||
|     XCircleIcon, | ||||
|     XCircleIcon | ||||
|   } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import Tr from "./Base/Tr.svelte" | ||||
|   import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte" | ||||
|  | @ -72,6 +72,7 @@ | |||
|   import HotkeyTable from "./BigComponents/HotkeyTable.svelte" | ||||
|   import SelectedElementPanel from "./Base/SelectedElementPanel.svelte" | ||||
|   import type { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" | ||||
|   import { GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
|   let layout = state.layout | ||||
|  | @ -98,22 +99,10 @@ | |||
|   }) | ||||
| 
 | ||||
|   let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => { | ||||
|     const id = element.properties.id | ||||
|     if (id.startsWith("current_view")) { | ||||
|     if (element.properties.id.startsWith("current_view")) { | ||||
|       return currentViewLayer | ||||
|     } | ||||
|     if (id.startsWith("summary_")) { | ||||
|       console.log("Not selecting a summary object. The summary object is", element) | ||||
|       return undefined | ||||
|     } | ||||
|     if (id.startsWith(LastClickFeatureSource.newPointElementId)) { | ||||
|       return layout.layers.find((l) => l.id === "last_click") | ||||
|     } | ||||
|     if (id === "location_track") { | ||||
|       return layout.layers.find((l) => l.id === "gps_track") | ||||
|     } | ||||
| 
 | ||||
|     return state.layout.getMatchingLayer(element.properties) | ||||
|     return state.getMatchingLayer(element.properties) | ||||
|   }) | ||||
|   let currentZoom = state.mapProperties.zoom | ||||
|   let showCrosshair = state.userRelatedState.showCrosshair | ||||
|  | @ -144,7 +133,7 @@ | |||
|     const bottomRight = mlmap.unproject([rect.right, rect.bottom]) | ||||
|     const bbox = new BBox([ | ||||
|       [topLeft.lng, topLeft.lat], | ||||
|       [bottomRight.lng, bottomRight.lat], | ||||
|       [bottomRight.lng, bottomRight.lat] | ||||
|     ]) | ||||
|     state.visualFeedbackViewportBounds.setData(bbox) | ||||
|   } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue