diff --git a/assets/layers/stairs/stairs.json b/assets/layers/stairs/stairs.json index 3a7441057..b30649e42 100644 --- a/assets/layers/stairs/stairs.json +++ b/assets/layers/stairs/stairs.json @@ -289,6 +289,35 @@ } ], "condition": "conveying!=yes" + }, + { + "id": "incline", + "render": { + "en": "These stairs have an incline of {incline}" + }, + "freeform": { + "key": "incline", + "type": "slope" + }, + "question": { + "en": "What is the incline of these stairs?" + }, + "mappings": [ + { + "if": "incline=up", + "then": { + "en": "The upward direction is {direction_absolute()}" + }, + "hideInAnswer": true + }, + { + "if": "incline=down", + "then": { + "en": "The downward direction is {direction_absolute()}" + }, + "hideInAnswer": true + } + ] } ] } diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts index 00d664da5..fb753b91a 100644 --- a/src/Logic/GeoOperations.ts +++ b/src/Logic/GeoOperations.ts @@ -19,6 +19,35 @@ import { Utils } from "../Utils" export class GeoOperations { private static readonly _earthRadius = 6378137 private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2 + private static readonly directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const + private static readonly directionsRelative = [ + "straight", + "slight_right", + "right", + "sharp_right", + "behind", + "sharp_left", + "left", + "slight_left", + ] as const + private static reverseBearing = { + N: 0, + NNE: 22.5, + NE: 45, + ENE: 67.5, + E: 90, + ESE: 112.5, + SE: 135, + SSE: 157.5, + S: 180, + SSW: 202.5, + SW: 225, + WSW: 247.5, + W: 270, + WNW: 292.5, + NW: 315, + NNW: 337.5, + } /** * Create a union between two features @@ -261,16 +290,16 @@ export class GeoOperations { } /** - * Generates the closest point on a way from a given point. - * If the passed-in geojson object is a polygon, the outer ring will be used as linestring - * - * The properties object will contain three values: - // - `index`: closest point was found on nth line part, - // - `dist`: distance between pt and the closest point (in kilometer), - // `location`: distance along the line between start (of the line) and the closest point. - * @param way The road on which you want to find a point - * @param point Point defined as [lon, lat] - */ + * Generates the closest point on a way from a given point. + * If the passed-in geojson object is a polygon, the outer ring will be used as linestring + * + * The properties object will contain three values: + // - `index`: closest point was found on nth line part, + // - `dist`: distance between pt and the closest point (in kilometer), + // `location`: distance along the line between start (of the line) and the closest point. + * @param way The road on which you want to find a point + * @param point Point defined as [lon, lat] + */ public static nearestPoint( way: Feature, point: [number, number] @@ -293,9 +322,11 @@ export class GeoOperations { * @param way */ public static forceLineString(way: Feature): Feature + public static forceLineString( way: Feature ): Feature + public static forceLineString( way: Feature ): Feature { @@ -800,6 +831,146 @@ export class GeoOperations { return { lon, lat } } + public static SplitSelfIntersectingWays(features: Feature[]): Feature[] { + const result: Feature[] = [] + + for (const feature of features) { + if (feature.geometry.type === "LineString") { + let coors = feature.geometry.coordinates + for (let i = coors.length - 1; i >= 0; i--) { + // Go back, to nick of the back when needed + const ci = coors[i] + for (let j = i + 1; j < coors.length; j++) { + const cj = coors[j] + if ( + Math.abs(ci[0] - cj[0]) <= 0.000001 && + Math.abs(ci[1] - cj[1]) <= 0.0000001 + ) { + // Found a self-intersecting way! + console.debug("SPlitting way", feature.properties.id) + result.push({ + ...feature, + geometry: { ...feature.geometry, coordinates: coors.slice(i + 1) }, + }) + coors = coors.slice(0, i + 1) + break + } + } + } + result.push({ + ...feature, + geometry: { ...feature.geometry, coordinates: coors }, + }) + } + } + + return result + } + + /** + * GeoOperations.distanceToHuman(52.8) // => "53m" + * GeoOperations.distanceToHuman(2800) // => "2.8km" + * GeoOperations.distanceToHuman(12800) // => "13km" + * + * @param meters + */ + public static distanceToHuman(meters: number): string { + if (meters === undefined) { + return "" + } + meters = Math.round(meters) + if (meters < 1000) { + return meters + "m" + } + + if (meters >= 10000) { + const km = Math.round(meters / 1000) + return km + "km" + } + + meters = Math.round(meters / 100) + const kmStr = "" + meters + + return kmStr.substring(0, kmStr.length - 1) + "." + kmStr.substring(kmStr.length - 1) + "km" + } + + /** + * GeoOperations.parseBearing("N") // => 0 + * GeoOperations.parseBearing("E") // => 90 + * GeoOperations.parseBearing("NE") // => 22.5 + * + * GeoOperations.parseBearing("90") // => 90 + * GeoOperations.parseBearing("-90°") // => 270 + * GeoOperations.parseBearing("180 °") // => 180 + * + */ + public static parseBearing(str: string | number) { + let n: number + if (typeof str === "string") { + str = str.trim() + if (str.endsWith("°")) { + str = str.substring(0, str.length - 1).trim() + } + n = Number(str) + } else { + n = str + } + if (!isNaN(n)) { + while (n < 0) { + n += 360 + } + return n % 360 + } + return GeoOperations.reverseBearing[str] + } + + /** + * GeoOperations.bearingToHuman(0) // => "N" + * GeoOperations.bearingToHuman(-9) // => "N" + * GeoOperations.bearingToHuman(-10) // => "N" + * GeoOperations.bearingToHuman(-180) // => "S" + * GeoOperations.bearingToHuman(181) // => "S" + * GeoOperations.bearingToHuman(46) // => "NE" + */ + public static bearingToHuman( + bearing: number + ): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" { + while (bearing < 0) { + bearing += 360 + } + bearing %= 360 + bearing += 22.5 + const segment = Math.floor(bearing / 45) % GeoOperations.directions.length + return GeoOperations.directions[segment] + } + + /** + * GeoOperations.bearingToHuman(0) // => "N" + * GeoOperations.bearingToHuman(-10) // => "N" + * GeoOperations.bearingToHuman(-180) // => "S" + * GeoOperations.bearingToHuman(181) // => "S" + * GeoOperations.bearingToHuman(46) // => "NE" + */ + public static bearingToHumanRelative( + bearing: number + ): + | "straight" + | "slight_right" + | "right" + | "sharp_right" + | "behind" + | "sharp_left" + | "left" + | "slight_left" { + while (bearing < 0) { + bearing += 360 + } + bearing %= 360 + bearing += 22.5 + const segment = Math.floor(bearing / 45) % GeoOperations.directionsRelative.length + return GeoOperations.directionsRelative[segment] + } + /** * Helper function which does the heavy lifting for 'inside' */ @@ -949,126 +1120,4 @@ export class GeoOperations { } throw "CalculateIntersection fallthrough: can not calculate an intersection between features" } - - public static SplitSelfIntersectingWays(features: Feature[]): Feature[] { - const result: Feature[] = [] - - for (const feature of features) { - if (feature.geometry.type === "LineString") { - let coors = feature.geometry.coordinates - for (let i = coors.length - 1; i >= 0; i--) { - // Go back, to nick of the back when needed - const ci = coors[i] - for (let j = i + 1; j < coors.length; j++) { - const cj = coors[j] - if ( - Math.abs(ci[0] - cj[0]) <= 0.000001 && - Math.abs(ci[1] - cj[1]) <= 0.0000001 - ) { - // Found a self-intersecting way! - console.debug("SPlitting way", feature.properties.id) - result.push({ - ...feature, - geometry: { ...feature.geometry, coordinates: coors.slice(i + 1) }, - }) - coors = coors.slice(0, i + 1) - break - } - } - } - result.push({ - ...feature, - geometry: { ...feature.geometry, coordinates: coors }, - }) - } - } - - return result - } - - /** - * GeoOperations.distanceToHuman(52.8) // => "53m" - * GeoOperations.distanceToHuman(2800) // => "2.8km" - * GeoOperations.distanceToHuman(12800) // => "13km" - * - * @param meters - */ - public static distanceToHuman(meters: number): string { - if (meters === undefined) { - return "" - } - meters = Math.round(meters) - if (meters < 1000) { - return meters + "m" - } - - if (meters >= 10000) { - const km = Math.round(meters / 1000) - return km + "km" - } - - meters = Math.round(meters / 100) - const kmStr = "" + meters - - return kmStr.substring(0, kmStr.length - 1) + "." + kmStr.substring(kmStr.length - 1) + "km" - } - - private static readonly directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const - private static readonly directionsRelative = [ - "straight", - "slight_right", - "right", - "sharp_right", - "behind", - "sharp_left", - "left", - "slight_left", - ] as const - - /** - * GeoOperations.bearingToHuman(0) // => "N" - * GeoOperations.bearingToHuman(-9) // => "N" - * GeoOperations.bearingToHuman(-10) // => "N" - * GeoOperations.bearingToHuman(-180) // => "S" - * GeoOperations.bearingToHuman(181) // => "S" - * GeoOperations.bearingToHuman(46) // => "NE" - */ - public static bearingToHuman( - bearing: number - ): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" { - while (bearing < 0) { - bearing += 360 - } - bearing %= 360 - bearing += 22.5 - const segment = Math.floor(bearing / 45) % GeoOperations.directions.length - return GeoOperations.directions[segment] - } - - /** - * GeoOperations.bearingToHuman(0) // => "N" - * GeoOperations.bearingToHuman(-10) // => "N" - * GeoOperations.bearingToHuman(-180) // => "S" - * GeoOperations.bearingToHuman(181) // => "S" - * GeoOperations.bearingToHuman(46) // => "NE" - */ - public static bearingToHumanRelative( - bearing: number - ): - | "straight" - | "slight_right" - | "right" - | "sharp_right" - | "behind" - | "sharp_left" - | "left" - | "slight_left" { - while (bearing < 0) { - bearing += 360 - } - bearing %= 360 - bearing += 22.5 - const segment = Math.floor(bearing / 45) % GeoOperations.directionsRelative.length - return GeoOperations.directionsRelative[segment] - } } diff --git a/src/Logic/SimpleMetaTagger.ts b/src/Logic/SimpleMetaTagger.ts index 2efcd6883..856de326b 100644 --- a/src/Logic/SimpleMetaTagger.ts +++ b/src/Logic/SimpleMetaTagger.ts @@ -144,6 +144,9 @@ class CountryTagger extends SimpleMetaTagger { tagsSource.data["_country"] = newCountry tagsSource?.ping() } else { + // We set, be we don't ping... this is for later + tagsSource.data["_country"] = newCountry + /** * What is this weird construction? * @@ -160,7 +163,6 @@ class CountryTagger extends SimpleMetaTagger { */ window.requestIdleCallback(() => { - tagsSource.data["_country"] = newCountry tagsSource?.ping() }) } @@ -478,10 +480,19 @@ export default class SimpleMetaTaggers { // isOpen is irrelevant return false } + if (feature.properties.opening_hours === undefined) { + return false + } if (feature.properties.opening_hours === "24/7") { feature.properties._isOpen = "yes" return true } + console.log( + "Calculating opening hours for", + feature.properties.name, + ":", + feature.properties.opening_hours + ) // _isOpen is calculated dynamically on every call Object.defineProperty(feature.properties, "_isOpen", { @@ -492,7 +503,8 @@ export default class SimpleMetaTaggers { if (tags.opening_hours === undefined) { return } - if (tags._country === undefined) { + const country = tags._country + if (country === undefined) { return } diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 6c2342a14..7464feb5f 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -1598,6 +1598,41 @@ export default class SpecialVisualizations { ) }, }, + { + funcName: "direction_absolute", + docs: "Converts compass degrees (with 0° being north, 90° being east, ...) into a human readable, translated direction such as 'north', 'northeast'", + args: [ + { + name: "key", + doc: "The attribute containing the degrees", + defaultValue: "_direction:centerpoint", + }, + ], + needsUrls: [], + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + args: string[], + feature: Feature, + layer: LayerConfig + ): BaseUIElement { + const key = args[0] === "" ? "_direction:centerpoint" : args[0] + return new VariableUiElement( + tagSource + .map((tags) => { + console.log("Direction value", tags[key], key) + return tags[key] + }) + .mapD((value) => { + const dir = GeoOperations.bearingToHuman( + GeoOperations.parseBearing(value) + ) + console.log("Human dir", dir) + return Translations.t.general.visualFeedback.directionsAbsolute[dir] + }) + ) + }, + }, ] specialVisualizations.push(new AutoApplyButton(specialVisualizations))