diff --git a/assets/svg/airport.svg b/assets/svg/airport.svg new file mode 100644 index 000000000..cbd2dc6f5 --- /dev/null +++ b/assets/svg/airport.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/svg/airport.svg.license b/assets/svg/airport.svg.license new file mode 100644 index 000000000..0cf6926f1 --- /dev/null +++ b/assets/svg/airport.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Maki +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json index c15cd4df8..0c3e03626 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -59,6 +59,16 @@ ], "sources": [] }, + { + "path": "airport.svg", + "license": "CC0-1.0", + "authors": [ + "Maki" + ], + "sources": [ + "https://github.com/mapbox/maki/blob/main/icons/airport.svg" + ] + }, { "path": "back.svg", "license": "CC0-1.0", @@ -1175,6 +1185,16 @@ "https://pngimg.com/image/46283" ] }, + { + "path": "train.svg", + "license": "CC0-1.0", + "authors": [ + "Maki" + ], + "sources": [ + "https://labs.mapbox.com/maki-icons/" + ] + }, { "path": "translate.svg", "license": "CC-BY-SA-3.0", diff --git a/assets/svg/train.svg b/assets/svg/train.svg new file mode 100644 index 000000000..fa72445c6 --- /dev/null +++ b/assets/svg/train.svg @@ -0,0 +1,24 @@ + +image/svg+xml + + diff --git a/assets/svg/train.svg.license b/assets/svg/train.svg.license new file mode 100644 index 000000000..0cf6926f1 --- /dev/null +++ b/assets/svg/train.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Maki +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/package.json b/package.json index c7b648130..b8dd3c55f 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,8 @@ ], "country_coder_host": "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country", "nominatimEndpoint": "https://geocoding.geofabrik.de/b75350b1cfc34962ac49824fe5b582dc/", + "#photonEndpoint": "`api/` or `reverse/` will be appended by the code", + "photonEndpoint": "https://photon.komoot.io/", "jsonld-proxy": "https://lod.mapcomplete.org/extractgraph?url={url}", "protomaps": { "api-key": "2af8b969a9e8b692", diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index df125fb0e..04137545c 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -1225,14 +1225,14 @@ video { height: 6rem; } -.h-screen { - height: 100vh; -} - .h-full { height: 100%; } +.h-screen { + height: 100vh; +} + .h-fit { height: -webkit-fit-content; height: -moz-fit-content; @@ -1284,10 +1284,6 @@ video { height: 2.75rem; } -.h-2\/3 { - height: 66.666667%; -} - .h-5 { height: 1.25rem; } @@ -2562,6 +2558,11 @@ video { border-color: rgb(209 213 219 / var(--tw-border-opacity)); } +.border-red-500 { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity)); +} + .border-gray-800 { --tw-border-opacity: 1; border-color: rgb(31 41 55 / var(--tw-border-opacity)); @@ -2657,11 +2658,6 @@ video { border-color: rgb(34 197 94 / var(--tw-border-opacity)); } -.border-red-500 { - --tw-border-opacity: 1; - border-color: rgb(239 68 68 / var(--tw-border-opacity)); -} - .border-gray-700 { --tw-border-opacity: 1; border-color: rgb(55 65 81 / var(--tw-border-opacity)); @@ -4636,7 +4632,6 @@ button.unstyled { display: inline-flex; justify-content: start; border: none; - border-radius: 0; box-shadow: none; margin: 0; padding: 0; diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 8fc470efb..8ec555847 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -327,8 +327,6 @@ class GenerateLayouts extends Script { ): Promise { const apiUrls: string[] = [ ...Constants.allServers, - Constants.countryCoderEndpoint, - Constants.nominatimEndpoint, "https://www.openstreetmap.org", "https://api.openstreetmap.org", "https://pietervdvn.goatcounter.com", diff --git a/src/Logic/BBox.ts b/src/Logic/BBox.ts index b54928418..c12357e26 100644 --- a/src/Logic/BBox.ts +++ b/src/Logic/BBox.ts @@ -6,7 +6,7 @@ import { Feature, Polygon } from "geojson" export class BBox { static global: BBox = new BBox([ [-180, -90], - [180, 90], + [180, 90] ]) readonly maxLat: number readonly maxLon: number @@ -53,7 +53,7 @@ export class BBox { static fromLeafletBounds(bounds) { return new BBox([ [bounds.getWest(), bounds.getNorth()], - [bounds.getEast(), bounds.getSouth()], + [bounds.getEast(), bounds.getSouth()] ]) } @@ -74,7 +74,7 @@ export class BBox { // Note: x is longitude f["bbox"] = new BBox([ [minX, minY], - [maxX, maxY], + [maxX, maxY] ]) } return f["bbox"] @@ -94,7 +94,7 @@ export class BBox { } return new BBox([ [maxLon, maxLat], - [minLon, minLat], + [minLon, minLat] ]) } @@ -121,7 +121,7 @@ export class BBox { public unionWith(other: BBox) { return new BBox([ [Math.max(this.maxLon, other.maxLon), Math.max(this.maxLat, other.maxLat)], - [Math.min(this.minLon, other.minLon), Math.min(this.minLat, other.minLat)], + [Math.min(this.minLon, other.minLon), Math.min(this.minLat, other.minLat)] ]) } @@ -174,7 +174,7 @@ export class BBox { return new BBox([ [lon - s / 2, lat - s / 2], - [lon + s / 2, lat + s / 2], + [lon + s / 2, lat + s / 2] ]) } @@ -231,21 +231,21 @@ export class BBox { const lonDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor) return new BBox([ [this.minLon - lonDiff, this.minLat - latDiff], - [this.maxLon + lonDiff, this.maxLat + latDiff], + [this.maxLon + lonDiff, this.maxLat + latDiff] ]) } padAbsolute(degrees: number): BBox { return new BBox([ [this.minLon - degrees, this.minLat - degrees], - [this.maxLon + degrees, this.maxLat + degrees], + [this.maxLon + degrees, this.maxLat + degrees] ]) } toLngLat(): [[number, number], [number, number]] { return [ [this.minLon, this.minLat], - [this.maxLon, this.maxLat], + [this.maxLon, this.maxLat] ] } @@ -260,7 +260,7 @@ export class BBox { return { type: "Feature", properties: properties, - geometry: this.asGeometry(), + geometry: this.asGeometry() } } @@ -273,9 +273,9 @@ export class BBox { [this.maxLon, this.minLat], [this.maxLon, this.maxLat], [this.minLon, this.maxLat], - [this.minLon, this.minLat], - ], - ], + [this.minLon, this.minLat] + ] + ] } } @@ -302,7 +302,7 @@ export class BBox { minLon, maxLon, minLat, - maxLat, + maxLat } } @@ -316,4 +316,8 @@ export class BBox { public overlapsWithFeature(f: Feature) { return GeoOperations.calculateOverlap(this.asGeoJson({}), [f]).length > 0 } + + center() { + return [(this.minLon + this.maxLon) / 2, (this.minLat + this.maxLat) / 2] + } } diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts index 7c3dabb6c..86743b22c 100644 --- a/src/Logic/GeoOperations.ts +++ b/src/Logic/GeoOperations.ts @@ -908,7 +908,7 @@ export class GeoOperations { } /** - * GeoOperations.distanceToHuman(52.8) // => "53m" + * GeoOperations.distanceToHuman(52.8) // => "50m" * GeoOperations.distanceToHuman(2800) // => "2.8km" * GeoOperations.distanceToHuman(12800) // => "13km" * @@ -920,11 +920,11 @@ export class GeoOperations { } meters = Math.round(meters) if (meters < 1000) { - return meters + "m" + return Utils.roundHuman(meters) + "m" } if (meters >= 10000) { - const km = Math.round(meters / 1000) + const km = Utils.roundHuman(meters / 1000) return km + "km" } diff --git a/src/Logic/Geocoding/CombinedSearcher.ts b/src/Logic/Geocoding/CombinedSearcher.ts index 68e0170ab..c26e445d3 100644 --- a/src/Logic/Geocoding/CombinedSearcher.ts +++ b/src/Logic/Geocoding/CombinedSearcher.ts @@ -9,13 +9,35 @@ export default class CombinedSearcher implements GeocodingProvider { this._providersWithSuggest = providers.filter(pr => pr.suggest !== undefined) } + /** + * Merges the geocode-results from various sources. + * If the same osm-id is mentioned multiple times, only the first result will be kept + * @param geocoded + * @private + */ + private merge(geocoded: GeoCodeResult[][]): GeoCodeResult[]{ + const results : GeoCodeResult[] = [] + const seenIds = new Set() + for (const geocodedElement of geocoded) { + for (const entry of geocodedElement) { + const id = entry.osm_type+ entry.osm_id + if(seenIds.has(id)){ + continue + } + seenIds.add(id) + results.push(entry) + } + } + return results + } + async search(query: string, options?: GeocodingOptions): Promise { const results = await Promise.all(this._providers.map(pr => pr.search(query, options))) - return results.flatMap(x => x) + return this.merge(results) } async suggest(query: string, options?: GeocodingOptions): Promise { const results = await Promise.all(this._providersWithSuggest.map(pr => pr.suggest(query, options))) - return results.flatMap(x => x) + return this.merge(results) } } diff --git a/src/Logic/Geocoding/CoordinateSearch.ts b/src/Logic/Geocoding/CoordinateSearch.ts index 6a0a789fe..84fddb452 100644 --- a/src/Logic/Geocoding/CoordinateSearch.ts +++ b/src/Logic/Geocoding/CoordinateSearch.ts @@ -44,7 +44,8 @@ export default class CoordinateSearch implements GeocodingProvider { lat: Number(m[1]), lon: Number(m[2]), display_name: "lon: " + m[2] + ", lat: " + m[1], - source: "coordinateSearch" + source: "coordinateSearch", + category: "coordinate" }) @@ -54,7 +55,8 @@ export default class CoordinateSearch implements GeocodingProvider { lat: Number(m[2]), lon: Number(m[1]), display_name: "lon: " + m[1] + ", lat: " + m[2], - source: "coordinateSearch" + source: "coordinateSearch", + category: "coordinate" }) return matches.concat(matchesLonLat) diff --git a/src/Logic/Geocoding/GeocodingProvider.ts b/src/Logic/Geocoding/GeocodingProvider.ts index 3d8ed7426..a174ef88c 100644 --- a/src/Logic/Geocoding/GeocodingProvider.ts +++ b/src/Logic/Geocoding/GeocodingProvider.ts @@ -1,8 +1,18 @@ import { BBox } from "../BBox" -import { Feature, FeatureCollection } from "geojson" +import { Feature, Geometry } from "geojson" +import { DefaultPinIcon } from "../../Models/Constants" + +export type GeocodingCategory = "coordinate" | "city" | "house" | "street" | "locality" | "country" | "train_station" | "county" | "airport" export type GeoCodeResult = { + /** + * The name of the feature being displayed + */ display_name: string + /** + * Some optional, extra information + */ + description?: string | Promise, feature?: Feature, lat: number lon: number @@ -12,7 +22,9 @@ export type GeoCodeResult = { */ boundingbox?: number[] osm_type?: "node" | "way" | "relation" - osm_id?: string + osm_id?: string, + category?: GeocodingCategory, + importance?: number } export interface GeocodingOptions { @@ -33,11 +45,52 @@ export default interface GeocodingProvider { suggest?(query: string, options?: GeocodingOptions): Promise } +export type ReverseGeocodingResult = Feature + export interface ReverseGeocodingProvider { reverseSearch( coordinate: { lon: number; lat: number }, zoom: number, language?: string - ): Promise ; + ): Promise ; +} + +export class GeocodingUtils { + + public static categoryToZoomLevel: Record = { + city: 12, + county: 10, + coordinate: 16, + country: 8, + house: 16, + locality: 14, + street: 15, + train_station: 14, + airport: 13 + + } + + + public static categoryToIcon: Record = { + city: "building_office_2", + coordinate: "globe_alt", + country: "globe_alt", + house: "house", + locality: "building_office_2", + street: "globe_alt", + train_station: "train", + county: "building_office_2", + airport: "airport" + + } + } diff --git a/src/Logic/Geocoding/LocalElementSearch.ts b/src/Logic/Geocoding/LocalElementSearch.ts index a39920ed9..434a46c8b 100644 --- a/src/Logic/Geocoding/LocalElementSearch.ts +++ b/src/Logic/Geocoding/LocalElementSearch.ts @@ -6,9 +6,11 @@ import { GeoOperations } from "../GeoOperations" export default class LocalElementSearch implements GeocodingProvider { private readonly _state: ThemeViewState + private readonly _limit: number - constructor(state: ThemeViewState) { + constructor(state: ThemeViewState, limit: number) { this._state = state + this._limit = limit } @@ -30,7 +32,8 @@ export default class LocalElementSearch implements GeocodingProvider { center: [number, number], levehnsteinD: number, physicalDistance: number, - searchTerms: string[] + searchTerms: string[], + description: string }[] = [] const properties = this._state.perLayer query = Utils.simplifyStringForSearch(query) @@ -51,19 +54,29 @@ export default class LocalElementSearch implements GeocodingProvider { })) const center = GeoOperations.centerpointCoordinates(feature) if (levehnsteinD <= 2) { + + let description = "" + function ifDef(prefix: string, key: string){ + if(feature.properties[key]){ + description += prefix+ feature.properties[key] + } + } + ifDef("", "addr:street") + ifDef(" ", "addr:housenumber") results.push({ feature, center, physicalDistance: GeoOperations.distanceBetween(centerPoint, center), levehnsteinD, - searchTerms + searchTerms, + description: description !== "" ? description : undefined }) } } } results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25)) - if (options?.limit) { - results = results.slice(0, options.limit) + if (this._limit || options?.limit) { + results = results.slice(0, Math.min(this._limit ?? options?.limit, options?.limit ?? this._limit)) } return results.map(entry => { const id = entry.feature.properties.id.split("/") @@ -74,7 +87,9 @@ export default class LocalElementSearch implements GeocodingProvider { osm_id: id[1], display_name: entry.searchTerms[0], source: "localElementSearch", - feature: entry.feature + feature: entry.feature, + importance: 1, + description: entry.description } }) } diff --git a/src/Logic/Geocoding/PhotonSearch.ts b/src/Logic/Geocoding/PhotonSearch.ts new file mode 100644 index 000000000..f268a1fab --- /dev/null +++ b/src/Logic/Geocoding/PhotonSearch.ts @@ -0,0 +1,137 @@ +import Constants from "../../Models/Constants" +import GeocodingProvider, { + GeoCodeResult, GeocodingCategory, + GeocodingOptions, + ReverseGeocodingProvider, + ReverseGeocodingResult +} from "./GeocodingProvider" +import { Utils } from "../../Utils" +import { Feature, FeatureCollection } from "geojson" +import Locale from "../../UI/i18n/Locale" +import { GeoOperations } from "../GeoOperations" + +export default class PhotonSearch implements GeocodingProvider, ReverseGeocodingProvider { + private _endpoint: string + private supportedLanguages = ["en", "de", "fr"] + private static readonly types = { + "R": "relation", + "W": "way", + "N": "node" + } + + + constructor(endpoint?: string) { + this._endpoint = endpoint ?? Constants.photonEndpoint ?? "https://photon.komoot.io/" + } + + async reverseSearch(coordinate: { + lon: number; + lat: number + }, zoom: number, language?: string): Promise { + const url = `${this._endpoint}/reverse?lon=${coordinate.lon}&lat=${coordinate.lat}&${this.getLanguage(language)}` + const result = await Utils.downloadJsonCached(url, 1000 * 60 * 60) + for (const f of result.features) { + f.properties.osm_type = PhotonSearch.types[f.properties.osm_type] + } + return result.features + + } + + /** + * Gets a `&lang=en` if the current/requested language is supported + * @param language + * @private + */ + private getLanguage(language?: string): string { + + language ??= Locale.language.data + if (this.supportedLanguages.indexOf(language) < 0) { + return "" + } + return `&lang=${language}` + + } + + search(query: string, options?: GeocodingOptions): Promise { + return this.suggest(query, options) + } + + private buildDescription(entry: Feature) { + const p = entry.properties + const type = p.type + + function ifdef(prefix: string, str: string) { + if (str) { + return prefix + str + } + return "" + } + + switch (type) { + case "house": { + + const addr = ifdef("", p.street) + ifdef(" ", p.housenumber) + if(!addr){ + return p.city + } + return addr + ifdef(", ", p.city) + } + case "coordinate": + case "street": + return p.city ?? p.country + case "city": + case "locality": + if(p.state){ + return p.state + ifdef(", ", p.country) + } + return p.country + case "country": + return undefined + } + + } + + private getCategory(entry: Feature){ + const p = entry.properties + if(p.osm_value === "train_station" || p.osm_key === "railway"){ + return "train_station" + } + if(p.osm_value === "aerodrome" || p.osm_key === "aeroway"){ + return "airport" + } + return p.type + } + + async suggest?(query: string, options?: GeocodingOptions): Promise { + if (query.length < 3) { + return [] + } + const limit = options?.limit ?? 5 + let bbox = "" + if (options?.bbox) { + const [lon, lat] = options.bbox.center() + bbox = `&lon=${lon}&lat=${lat}` + } + const url = `${this._endpoint}/api/?q=${encodeURIComponent(query)}&limit=${limit}${this.getLanguage()}${bbox}` + const results = await Utils.downloadJsonCached(url, 1000 * 60 * 60) + return results.features.map(f => { + const [lon, lat] = GeoOperations.centerpointCoordinates(f) + let boundingbox: number[] = undefined + if (f.properties.extent) { + const [lon0, lat0, lon1, lat1] = f.properties.extent + boundingbox = [lat0, lat1, lon0, lon1] + } + return { + feature: f, + osm_id: f.properties.osm_id, + display_name: f.properties.name, + description: this.buildDescription(f), + osm_type: PhotonSearch.types[f.properties.osm_type], + category: this.getCategory(f), + boundingbox, + lon, lat + } + }) + } + +} diff --git a/src/Logic/Geocoding/RecentSearch.ts b/src/Logic/Geocoding/RecentSearch.ts new file mode 100644 index 000000000..5ba22bca3 --- /dev/null +++ b/src/Logic/Geocoding/RecentSearch.ts @@ -0,0 +1,49 @@ +import { Store, UIEventSource } from "../UIEventSource" +import { Feature } from "geojson" +import { OsmConnection } from "../Osm/OsmConnection" +import { GeoCodeResult } from "./GeocodingProvider" +import { GeoOperations } from "../GeoOperations" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" + +export class RecentSearch { + + private readonly _recentSearches: UIEventSource + public readonly recentSearches: Store + + private readonly _seenThisSession: UIEventSource = new UIEventSource([]) + public readonly seenThisSession: Store = this._seenThisSession + + constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store }) { + const longPref = state.osmConnection.preferencesHandler.GetLongPreference("recent-searches") + this._recentSearches = longPref.sync(str => !str ? [] : JSON.parse(str), [], strs => JSON.stringify(strs)) + this.recentSearches = this._recentSearches + + state.selectedElement.addCallbackAndRunD(selected => { + const [osm_type, osm_id] = selected.properties.id.split("/") + const [lon, lat] = GeoOperations.centerpointCoordinates(selected) + const entry = { + feature: selected, + osm_id, osm_type, + description: "Viewed recently", + lon, lat + } + this.addSelected(entry) + + }) + } + + addSelected(entry: GeoCodeResult) { + const arr = [...this.seenThisSession.data.slice(0, 20), entry] + + const seenIds = new Set() + for (let i = arr.length - 1; i >= 0; i--) { + const id = arr[i].osm_type + arr[i].osm_id + if (seenIds.has(id)) { + arr.splice(i, 1) + } else { + seenIds.add(id) + } + } + this._seenThisSession.set(arr) + } +} diff --git a/src/Logic/MetaTagging.ts b/src/Logic/MetaTagging.ts index 55f78d96c..e77ea8c91 100644 --- a/src/Logic/MetaTagging.ts +++ b/src/Logic/MetaTagging.ts @@ -424,9 +424,11 @@ export default class MetaTagging { } } - console.warn( - "Static MetataggingObject for theme is not set; using `new Function` (aka `eval`) to get calculated tags. This might trip up the CSP" - ) + if (!window.location.pathname.endsWith("theme.html")) { + console.warn( + "Static MetataggingObject for theme is not set; using `new Function` (aka `eval`) to get calculated tags. This might trip up the CSP" + ) + } const calculatedTags: [string, string, boolean][] = layer?.calculatedTags ?? [] if (calculatedTags === undefined || calculatedTags.length === 0) { diff --git a/src/Logic/UIEventSource.ts b/src/Logic/UIEventSource.ts index a8dd7923a..4fc839405 100644 --- a/src/Logic/UIEventSource.ts +++ b/src/Logic/UIEventSource.ts @@ -617,7 +617,7 @@ export class UIEventSource extends Store implements Writable { } /** - * Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated. + * Converts a promise into a UIventsource, sets the UIeventSource when the result is calculated. * If the promise fails, the value will stay undefined, but 'onError' will be called */ public static FromPromise( diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts index e0fcc7a66..4cada3e19 100644 --- a/src/Models/Constants.ts +++ b/src/Models/Constants.ts @@ -4,6 +4,7 @@ import { Utils } from "../Utils" import { AuthConfig } from "../Logic/Osm/AuthConfig" export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number] +export type DefaultPinIcon = (typeof Constants._defaultPinIcons)[number] export default class Constants { public static vNumber: string = packagefile.version @@ -113,14 +114,18 @@ export default class Constants { public static countryCoderEndpoint: string = Constants.config.country_coder_host public static osmAuthConfig: AuthConfig = Constants.config.oauth_credentials public static nominatimEndpoint: string = Constants.config.nominatimEndpoint + public static photonEndpoint: string = Constants.config.photonEndpoint + public static linkedDataProxy: string = Constants.config["jsonld-proxy"] /** * These are the values that are allowed to use as 'backdrop' icon for a map pin */ - private static readonly _defaultPinIcons = [ + public static readonly _defaultPinIcons = [ "addSmall", + "airport", "brick_wall_round", "brick_wall_square", + "building_office_2", "bug", "checkmark", "checkmark", @@ -135,12 +140,14 @@ export default class Constants { "desktop", "direction", "gear", + "globe_alt", "gps_arrow", "heart", "heart_outline", "help", "help", "home", + "house", "invalid", "invalid", "link", @@ -160,7 +167,9 @@ export default class Constants { "square_rounded", "teardrop", "teardrop_with_hole_green", + "train", "triangle", + "user_circle", "wifi", ] as const public static readonly defaultPinIcons: string[] = Constants._defaultPinIcons @@ -183,6 +192,7 @@ export default class Constants { Constants.countryCoderEndpoint, Constants.osmAuthConfig.url, Constants.nominatimEndpoint, + Constants.photonEndpoint, Constants.linkedDataProxy, ...Constants.defaultOverpassUrls, ] diff --git a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts index eade83f77..3adb3a96f 100644 --- a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts +++ b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts @@ -321,7 +321,13 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs editButtonAriaLabel?: Translatable /** + * What labels should be applied on this tagRendering? + * * A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer + * + * Special values: + * - "hidden": do not show this tagRendering. Useful in it is used by e.g. an accordion + * - "description": this label is a description used in the search */ labels?: string[] } diff --git a/src/Models/ThemeConfig/TagRenderingConfig.ts b/src/Models/ThemeConfig/TagRenderingConfig.ts index 7dd8f07b7..a9b7bca30 100644 --- a/src/Models/ThemeConfig/TagRenderingConfig.ts +++ b/src/Models/ThemeConfig/TagRenderingConfig.ts @@ -898,9 +898,7 @@ export default class TagRenderingConfig { ].join("\n") } - public - - usedTags(): TagsFilter[] { + public usedTags(): TagsFilter[] { const tags: TagsFilter[] = [] tags.push( this.metacondition, diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 617d09498..a9d9bfd0c 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -79,6 +79,8 @@ import CombinedSearcher from "../Logic/Geocoding/CombinedSearcher" import { NominatimGeocoding } from "../Logic/Geocoding/NominatimGeocoding" import CoordinateSearch from "../Logic/Geocoding/CoordinateSearch" import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch" +import { RecentSearch } from "../Logic/Geocoding/RecentSearch" +import PhotonSearch from "../Logic/Geocoding/PhotonSearch" /** * @@ -160,6 +162,7 @@ export default class ThemeViewState implements SpecialVisualizationState { public readonly nearbyImageSearcher: CombinedFetcher public readonly geosearch: GeocodingProvider + public readonly recentlySearched: RecentSearch constructor(layout: LayoutConfig, mvtAvailableLayers: Set) { Utils.initDomPurify() @@ -387,11 +390,12 @@ export default class ThemeViewState implements SpecialVisualizationState { this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined this.geosearch = new CombinedSearcher( - new NominatimGeocoding(), + new LocalElementSearch(this, 5), + new PhotonSearch(), // new NominatimGeocoding(), new CoordinateSearch(), - new LocalElementSearch(this) ) + this.recentlySearched = new RecentSearch(this) this.initActors() this.drawSpecialLayers() diff --git a/src/UI/BigComponents/Geosearch.svelte b/src/UI/BigComponents/Geosearch.svelte index 308743ac0..9b13c9351 100644 --- a/src/UI/BigComponents/Geosearch.svelte +++ b/src/UI/BigComponents/Geosearch.svelte @@ -1,5 +1,5 @@ -
+
{#if isRunning} {Translations.t.general.search.searching} @@ -138,7 +135,9 @@ feedback = undefined return keypr.key === "Enter" ? performSearch() : undefined }} - bind:value={searchContents} + on:focus={() => {isFocused.setData(true)}} + on:blur={() => {isFocused.setData(false)}} + bind:value={$searchContents} use:placeholder={Translations.t.general.search.search} use:ariaLabel={Translations.t.general.search.search} /> @@ -153,6 +152,9 @@
-
- + diff --git a/src/UI/BigComponents/SearchResult.svelte b/src/UI/BigComponents/SearchResult.svelte index 8cd67cd5c..84a38cb1e 100644 --- a/src/UI/BigComponents/SearchResult.svelte +++ b/src/UI/BigComponents/SearchResult.svelte @@ -1,46 +1,84 @@ - diff --git a/src/UI/BigComponents/SearchResults.svelte b/src/UI/BigComponents/SearchResults.svelte index 124e1545b..a4bd226b1 100644 --- a/src/UI/BigComponents/SearchResults.svelte +++ b/src/UI/BigComponents/SearchResults.svelte @@ -3,25 +3,79 @@ import SearchResult from "./SearchResult.svelte" import type { SpecialVisualizationState } from "../SpecialVisualization" import { XMarkIcon } from "@babeard/svelte-heroicons/solid" + import { Store } from "../../Logic/UIEventSource" + import Loading from "../Base/Loading.svelte" export let state: SpecialVisualizationState export let results: GeoCodeResult[] + export let searchTerm: Store + export let isFocused: Store - function close(){ - results = [] - } + let recentlySeen: Store = state.recentlySearched.seenThisSession -{#if results.length > 0} -
+
+ {#if $searchTerm.length > 0 && results === undefined} + + {:else if results?.length > 0} +
+ -
close()}> - -
-
-{/if} + {:else } + + + {/if} +
+ + + diff --git a/src/UI/Map/Icon.svelte b/src/UI/Map/Icon.svelte index 2fa9d5145..21bb340ac 100644 --- a/src/UI/Map/Icon.svelte +++ b/src/UI/Map/Icon.svelte @@ -23,7 +23,7 @@ import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte" import Gps_arrow from "../../assets/svg/Gps_arrow.svelte" import { HeartIcon, PencilIcon, WifiIcon } from "@babeard/svelte-heroicons/solid" - import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline" + import { HeartIcon as HeartOutlineIcon, HomeIcon } from "@babeard/svelte-heroicons/outline" import Confirm from "../../assets/svg/Confirm.svelte" import Not_found from "../../assets/svg/Not_found.svelte" import { twMerge } from "tailwind-merge" @@ -31,7 +31,7 @@ import Mastodon from "../../assets/svg/Mastodon.svelte" import Party from "../../assets/svg/Party.svelte" import AddSmall from "../../assets/svg/AddSmall.svelte" - import { LinkIcon } from "@babeard/svelte-heroicons/mini" + import { GlobeAltIcon, LinkIcon } from "@babeard/svelte-heroicons/mini" import Square_rounded from "../../assets/svg/Square_rounded.svelte" import Bug from "../../assets/svg/Bug.svelte" import Cross_bottom_right from "../../assets/svg/Cross_bottom_right.svelte" @@ -39,6 +39,9 @@ import Gear from "../../assets/svg/Gear.svelte" import { DesktopComputerIcon, UserCircleIcon } from "@rgossiaux/svelte-heroicons/solid" import Relocation from "../../assets/svg/Relocation.svelte" + import BuildingOffice2 from "@babeard/svelte-heroicons/outline/BuildingOffice2" + import Train from "../../assets/svg/Train.svelte" + import Airport from "../../assets/svg/Airport.svelte" /** * Renders a single icon. @@ -146,10 +149,21 @@ {:else if icon === "user_circle"} + {:else if icon==="globe_alt"} + + {:else if icon === "building_office_2"} + + {:else if icon === "house"} + + {:else if icon === "train"} + + {:else if icon === "airport"} + {:else if Utils.isEmoji(icon)} {icon} + {:else} {/if} diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index 72699ad0f..fa656b3f1 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -29,6 +29,7 @@ import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" import { Map as MlMap } from "maplibre-gl" import ShowDataLayer from "./Map/ShowDataLayer" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" +import { RecentSearch } from "../Logic/Geocoding/RecentSearch" /** * The state needed to render a special Visualisation. @@ -95,6 +96,8 @@ export interface SpecialVisualizationState { readonly previewedImage: UIEventSource readonly nearbyImageSearcher: CombinedFetcher readonly geolocation: GeoLocationHandler + readonly recentlySearched: RecentSearch + showCurrentLocationOn(map: Store): ShowDataLayer reportError(message: string): Promise diff --git a/src/Utils.ts b/src/Utils.ts index eb8503387..e8a8d1400 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -114,7 +114,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be "version", "wayHandling", "widenFactor", - "width", + "width" ] private static extraKeys = [ "nl", @@ -133,7 +133,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be "yes", "no", "true", - "false", + "false" ] private static injectedDownloads = {} private static _download_cache = new Map< @@ -150,7 +150,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be if (Utils.runningFromConsole) { return } - DOMPurify.addHook("afterSanitizeAttributes", function (node) { + DOMPurify.addHook("afterSanitizeAttributes", function(node) { // set all elements owning target to target=_blank + add noopener noreferrer const target = node.getAttribute("target") if (target) { @@ -163,7 +163,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static purify(src: string): string { return DOMPurify.sanitize(src, { USE_PROFILES: { html: true }, - ADD_ATTR: ["target"], // Don't remove target='_blank'. Note that Utils.initDomPurify does add a hook which automatically adds 'rel=noopener' + ADD_ATTR: ["target"] // Don't remove target='_blank'. Note that Utils.initDomPurify does add a hook which automatically adds 'rel=noopener' }) } @@ -193,7 +193,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be parsed[spec.name] = arg } - return parsed + return parsed } static EncodeXmlValue(str) { @@ -344,7 +344,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be console.error("Error while calculating a lazy property", e) return undefined } - }, + } }) } @@ -368,7 +368,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be whenDone() } }) - }, + } }) } @@ -651,7 +651,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be if (!Array.isArray(targetV)) { throw new Error( "Cannot concatenate: value to add is not an array: " + - JSON.stringify(targetV) + JSON.stringify(targetV) ) } if (Array.isArray(sourceV)) { @@ -659,9 +659,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } else { throw new Error( "Could not merge concatenate " + - JSON.stringify(sourceV) + - " and " + - JSON.stringify(targetV) + JSON.stringify(sourceV) + + " and " + + JSON.stringify(targetV) ) } } else { @@ -922,7 +922,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be continue } const i = part.charCodeAt(0) - result += '"' + keys[i] + '":' + part.substring(1) + result += "\"" + keys[i] + "\":" + part.substring(1) } return result @@ -1000,7 +1000,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be resolve({ error: "other error: " + xhr.statusText + ", " + xhr.responseText, url, - statuscode: xhr.status, + statuscode: xhr.status }) } } @@ -1014,12 +1014,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be xhr.onerror = (ev: ProgressEvent) => reject( "Could not get " + - url + - ", xhr status code is " + - xhr.status + - " (" + - xhr.statusText + - ")" + url + + ", xhr status code is " + + xhr.status + + " (" + + xhr.statusText + + ")" ) }) } @@ -1077,12 +1077,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } const promise = /*NO AWAIT as we work with the promise directly */ Utils.downloadJsonAdvanced( - url, - headers - ) + url, + headers + ) Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() }) return await promise } + public static async downloadJson( url: string, headers?: Record @@ -1271,7 +1272,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be ): T[] { const withDistance: [T, number][] = ts.map((t) => [ t, - Utils.levenshteinDistance(getName(t), reference), + Utils.levenshteinDistance(getName(t), reference) ]) withDistance.sort(([_, a], [__, b]) => a - b) return withDistance.map((n) => n[0]) @@ -1393,7 +1394,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return { r: Utils.percentageToNumber(match[1]), g: Utils.percentageToNumber(match[2]), - b: Utils.percentageToNumber(match[3]), + b: Utils.percentageToNumber(match[3]) } } @@ -1404,14 +1405,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return { r: parseInt(hex.substr(1, 1), 16), g: parseInt(hex.substr(2, 1), 16), - b: parseInt(hex.substr(3, 1), 16), + b: parseInt(hex.substr(3, 1), 16) } } return { r: parseInt(hex.substr(1, 2), 16), g: parseInt(hex.substr(3, 2), 16), - b: parseInt(hex.substr(5, 2), 16), + b: parseInt(hex.substr(5, 2), 16) } } @@ -1586,7 +1587,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be line: Number(line), column: Number(column), markdownLocation, - filename: path.substring(path.lastIndexOf("/") + 1), + filename: path.substring(path.lastIndexOf("/") + 1) } } @@ -1611,8 +1612,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * Utils.simplifyStringForSearch("abc def; ghi 564") // => "abcdefghi564" * Utils.simplifyStringForSearch("âbc déf; ghi 564") // => "abcdefghi564" */ - public static simplifyStringForSearch(str: string): string{ - return Utils.RemoveDiacritics(str) .toLowerCase().replace(/[^a-z0-9]/g, "") + public static simplifyStringForSearch(str: string): string { + return Utils.RemoveDiacritics(str).toLowerCase().replace(/[^a-z0-9]/g, "") } public static randomString(length: number): string { @@ -1723,6 +1724,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } private static readonly _metrixPrefixes = ["", "k", "M", "G", "T", "P", "E"] + /** * Converts a big number (e.g. 1000000) into a rounded postfixed verion (e.g. 1M) * @@ -1737,6 +1739,34 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return n + Utils._metrixPrefixes[index] } + /** + * Rounds to a human-number + * @param number + * + * Utils.roundHuman(7) // => 7 + * Utils.roundHuman(147) // => 150 + * Utils.roundHuman(386) // => 375 + * Utils.roundHuman(521) // => 500 + */ + public static roundHuman(number: number) { + if (number <= 25) { + return number + } + if (number < 100) { + return 5 * Math.round(number / 5) + } + if (number < 250) { + return 10 * Math.round(number / 10) + + } + if (number < 500) { + return 25 * Math.round(number / 25) + + } + return 50 * Math.round(number / 50) + + } + static NoNullInplace(layers: any[]): void { for (let i = layers.length - 1; i >= 0; i--) { if (layers[i] === null || layers[i] === undefined) { diff --git a/src/assets/svg/Airport.svelte b/src/assets/svg/Airport.svelte new file mode 100644 index 000000000..b56c711d0 --- /dev/null +++ b/src/assets/svg/Airport.svelte @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/src/assets/svg/Train.svelte b/src/assets/svg/Train.svelte new file mode 100644 index 000000000..8718befad --- /dev/null +++ b/src/assets/svg/Train.svelte @@ -0,0 +1,4 @@ + + image/svg+xml \ No newline at end of file