2021-03-24 01:25:57 +01:00
import { GeoOperations } from "./GeoOperations"
import { Utils } from "../Utils"
import opening_hours from "opening_hours"
2021-10-12 02:12:45 +02:00
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
2021-12-07 02:22:56 +01:00
import { CountryCoder } from "latlon2country"
2022-04-28 00:29:22 +02:00
import Constants from "../Models/Constants"
2022-07-22 01:33:11 +02:00
import { TagUtils } from "./Tags/TagUtils"
2025-02-04 01:06:55 +01:00
import { Feature , LineString , MultiPolygon , Polygon } from "geojson"
2023-03-26 05:58:28 +02:00
import { OsmTags } from "../Models/OsmFeature"
import { UIEventSource } from "./UIEventSource"
2024-10-17 04:06:03 +02:00
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
2023-04-20 03:58:31 +02:00
import OsmObjectDownloader from "./Osm/OsmObjectDownloader"
2023-07-17 20:25:19 +02:00
import countryToCurrency from "country-to-currency"
2021-04-22 13:30:00 +02:00
2023-03-28 05:13:48 +02:00
/ * *
* All elements that are needed to perform metatagging
* /
export interface MetataggingState {
2024-10-17 04:06:03 +02:00
theme : ThemeConfig
2023-04-20 03:58:31 +02:00
osmObjectDownloader : OsmObjectDownloader
2023-03-28 05:13:48 +02:00
}
2023-03-26 05:58:28 +02:00
export abstract class SimpleMetaTagger {
2021-12-07 02:22:56 +01:00
public readonly keys : string [ ]
public readonly doc : string
public readonly isLazy : boolean
public readonly includesDates : boolean
/ * * *
* A function that adds some extra data to a feature
2024-05-24 15:39:32 +02:00
* @param docs what does this extra data do ?
2021-12-07 02:22:56 +01:00
* /
2023-03-26 05:58:28 +02:00
protected constructor ( docs : {
keys : string [ ]
doc : string
/ * *
* Set this flag if the data is volatile or date - based .
* It 'll _won' t_ be cached in this case
* /
includesDates? : boolean
isLazy? : boolean
cleanupRetagger? : boolean
} ) {
2021-12-07 02:22:56 +01:00
this . keys = docs . keys
this . doc = docs . doc
this . isLazy = docs . isLazy
this . includesDates = docs . includesDates ? ? false
if ( ! docs . cleanupRetagger ) {
for ( const key of docs . keys ) {
if ( ! key . startsWith ( "_" ) && key . toLowerCase ( ) . indexOf ( "theme" ) < 0 ) {
2022-07-22 01:33:11 +02:00
throw ` Incorrect key for a calculated meta value ' ${ key } ': it should start with underscore (_) `
2021-12-07 02:22:56 +01:00
}
}
}
}
2023-03-26 05:58:28 +02:00
/ * *
* Applies the metatag - calculation , returns 'true' if the upstream source needs to be pinged
* @param feature
* @param layer
* @param tagsStore
* @param state
* /
public abstract applyMetaTagsOnFeature (
2024-05-24 15:39:32 +02:00
feature : Feature ,
2023-03-26 05:58:28 +02:00
layer : LayerConfig ,
tagsStore : UIEventSource < Record < string , string > > ,
2023-03-28 05:13:48 +02:00
state : MetataggingState
2023-03-26 05:58:28 +02:00
) : boolean
2021-12-07 02:22:56 +01:00
}
2023-02-09 00:10:59 +01:00
export class ReferencingWaysMetaTagger extends SimpleMetaTagger {
/ * *
* Disable this metatagger , e . g . for caching or tests
* This is a bit a work - around
* /
public static enabled = true
2023-03-26 05:58:28 +02:00
2023-02-09 00:10:59 +01:00
constructor ( ) {
2023-03-26 05:58:28 +02:00
super ( {
keys : [ "_referencing_ways" ] ,
isLazy : true ,
2025-02-10 02:04:58 +01:00
doc : "_referencing_ways contains - for a node - which ways use this node as point in their geometry. " ,
2023-03-26 05:58:28 +02:00
} )
}
2023-02-09 00:10:59 +01:00
2023-03-26 05:58:28 +02:00
public applyMetaTagsOnFeature ( feature , layer , tags , state ) {
if ( ! ReferencingWaysMetaTagger . enabled ) {
return false
}
//this function has some extra code to make it work in SimpleAddUI.ts to also work for newly added points
const id = feature . properties . id
if ( ! id . startsWith ( "node/" ) ) {
return false
}
2023-04-14 17:53:08 +02:00
2023-04-15 02:28:24 +02:00
Utils . AddLazyPropertyAsync ( feature . properties , "_referencing_ways" , async ( ) = > {
2023-04-20 03:58:31 +02:00
const referencingWays = await state . osmObjectDownloader . DownloadReferencingWays ( id )
2023-03-26 05:58:28 +02:00
const wayIds = referencingWays . map ( ( w ) = > "way/" + w . id )
wayIds . sort ( )
2023-04-15 02:28:24 +02:00
return wayIds . join ( ";" )
2023-03-26 05:58:28 +02:00
} )
return true
2023-02-09 00:10:59 +01:00
}
}
2023-04-14 17:53:08 +02:00
class CountryTagger extends SimpleMetaTagger {
2022-04-28 00:29:22 +02:00
private static readonly coder = new CountryCoder (
Constants . countryCoderEndpoint ,
Utils . downloadJson
2022-09-08 21:40:48 +02:00
)
2024-05-24 15:39:32 +02:00
public runningTasks : Set < Feature > = new Set < Feature > ( )
2022-01-26 20:47:08 +01:00
2021-12-07 02:22:56 +01:00
constructor ( ) {
2023-03-26 05:58:28 +02:00
super ( {
keys : [ "_country" ] ,
2024-05-13 17:14:16 +02:00
doc : "The country codes of the of the country/countries that the feature is located in (with latlon2country). Might contain _multiple_ countries, separated by a `;`" ,
2025-02-10 02:04:58 +01:00
includesDates : false ,
2023-03-26 05:58:28 +02:00
} )
}
2024-05-24 15:39:32 +02:00
applyMetaTagsOnFeature ( feature : Feature , _ , tagsSource ) {
2023-03-26 05:58:28 +02:00
const runningTasks = this . runningTasks
2024-06-16 16:06:26 +02:00
if ( runningTasks . has ( feature ) || ! ! feature . properties . _country ) {
2024-05-27 01:23:49 +02:00
return
}
2023-03-26 05:58:28 +02:00
runningTasks . add ( feature )
2024-05-27 01:23:49 +02:00
const [ lon , lat ] = GeoOperations . centerpointCoordinates ( feature )
2023-03-26 05:58:28 +02:00
CountryTagger . coder
. GetCountryCodeAsync ( lon , lat )
. then ( ( countries ) = > {
2023-06-14 20:39:36 +02:00
if ( ! countries ) {
2024-05-27 01:23:49 +02:00
console . warn ( "Country coder returned weird value" , countries )
2023-06-09 16:13:35 +02:00
return
}
2024-05-13 17:14:16 +02:00
const newCountry = countries . join ( ";" ) . trim ( ) . toLowerCase ( )
2024-05-27 01:23:49 +02:00
const oldCountry = feature . properties [ "_country" ]
2023-03-28 05:13:48 +02:00
if ( oldCountry !== newCountry ) {
2023-12-12 13:28:07 +01:00
if ( typeof window === undefined ) {
tagsSource . data [ "_country" ] = newCountry
tagsSource ? . ping ( )
} else {
2023-12-31 14:09:25 +01:00
// We set, be we don't ping... this is for later
tagsSource . data [ "_country" ] = newCountry
2023-12-12 13:28:07 +01:00
/ * *
* What is this weird construction ?
*
2024-05-13 17:14:16 +02:00
* For a theme with hundreds of items ( e . g . shops )
2023-12-12 13:28:07 +01:00
* the country for all those shops will probably arrive at the same time .
*
* This means that all those stores will be pinged around the same time .
* Now , the country is pivotal in calculating the opening hours ( because opening hours need the country to determine e . g . public holidays ) .
*
* In other words , when the country information becomes available , it ' ll start calculating the opening hours for hundreds of items at the same time .
* This will choke up the main thread for at least a few seconds , causing a very annoying hang .
*
* As such , we use 'requestIdleCallback' instead to gently spread out these calculations
* /
window . requestIdleCallback ( ( ) = > {
tagsSource ? . ping ( )
} )
}
2023-03-26 05:58:28 +02:00
}
} )
2023-03-28 05:13:48 +02:00
. catch ( ( e ) = > {
console . warn ( e )
2023-03-26 05:58:28 +02:00
} )
2023-03-28 05:13:48 +02:00
. finally ( ( ) = > runningTasks . delete ( feature ) )
2023-03-26 05:58:28 +02:00
return false
2021-12-07 02:22:56 +01:00
}
Fix issues with camera rotation
This commit fixes at least these issues that I was aware of:
* Cardinal directions (e.g. NE) were not recognized.
* The camera icon did not rotatie when direction=* was used instead of
camera:direction, but the blue direction visualizer did.
Pietervdvn said he would have liked to convert the code for direction
normalizing to calculatedTags in a JSON file (as documented in
Docs/CalculatedTags.md), but when he saw the oneliners I had to produce
in response, I was allowed to keep it in SimpleMetaTagger.ts for now.
For your amusement, the oneliners are included below.
"calculatedTags": [
"_direction:numerical=(dir => dir === undefined ? undefined : ({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}[dir] ?? (isNaN(parseFloat(dir)) ? undefined : ((parseFloat(dir) % 360 + 360) % 360)))))(feat.properties['camera:direction'] ?? feat.properties.direction)",
"_direction:leftright=feat.properties['_direction:numerical'] === undefined ? undefined : (feat.properties['_direction:numerical'] <= 180 ? 'right' : 'left')"
]
2021-04-28 16:45:48 +02:00
}
2023-03-26 05:58:28 +02:00
class InlineMetaTagger extends SimpleMetaTagger {
2023-03-28 05:13:48 +02:00
public readonly applyMetaTagsOnFeature : (
2024-05-24 15:39:32 +02:00
feature : Feature ,
2023-03-28 05:13:48 +02:00
layer : LayerConfig ,
tagsStore : UIEventSource < OsmTags > ,
state : MetataggingState
) = > boolean
2023-03-26 05:58:28 +02:00
constructor (
docs : {
keys : string [ ]
doc : string
/ * *
* Set this flag if the data is volatile or date - based .
* It 'll _won' t_ be cached in this case
* /
includesDates? : boolean
isLazy? : boolean
cleanupRetagger? : boolean
} ,
f : (
2024-05-24 15:39:32 +02:00
feature : Feature ,
2023-03-26 05:58:28 +02:00
layer : LayerConfig ,
tagsStore : UIEventSource < OsmTags > ,
2023-03-28 05:13:48 +02:00
state : MetataggingState
2023-03-26 05:58:28 +02:00
) = > boolean
) {
super ( docs )
this . applyMetaTagsOnFeature = f
}
}
2023-03-28 05:13:48 +02:00
2023-04-14 17:53:08 +02:00
class RewriteMetaInfoTags extends SimpleMetaTagger {
2023-03-28 05:13:48 +02:00
constructor ( ) {
super ( {
2021-04-25 13:25:03 +02:00
keys : [
"_last_edit:contributor" ,
"_last_edit:contributor:uid" ,
"_last_edit:changeset" ,
"_last_edit:timestamp" ,
2021-10-16 02:54:22 +02:00
"_version_number" ,
2025-02-10 02:04:58 +01:00
"_backend" ,
2021-10-22 01:42:44 +02:00
] ,
2025-02-10 02:04:58 +01:00
doc : "Information about the last edit of this object. This object will actually _rewrite_ some tags for features coming from overpass" ,
2023-03-28 05:13:48 +02:00
} )
}
2021-04-25 13:25:03 +02:00
2023-03-28 05:13:48 +02:00
applyMetaTagsOnFeature ( feature : Feature ) : boolean {
/*Note: also called by 'UpdateTagsFromOsmAPI'*/
2021-06-15 00:28:59 +02:00
2023-03-28 05:13:48 +02:00
const tgs = feature . properties
let movedSomething = false
2021-06-15 00:28:59 +02:00
2023-03-28 05:13:48 +02:00
function move ( src : string , target : string ) {
if ( tgs [ src ] === undefined ) {
return
}
tgs [ target ] = tgs [ src ]
delete tgs [ src ]
movedSomething = true
2021-04-25 13:25:03 +02:00
}
2023-03-28 05:13:48 +02:00
move ( "user" , "_last_edit:contributor" )
move ( "uid" , "_last_edit:contributor:uid" )
move ( "changeset" , "_last_edit:changeset" )
move ( "timestamp" , "_last_edit:timestamp" )
move ( "version" , "_version_number" )
2024-08-01 15:48:31 +02:00
feature . properties . _backend ? ? = "https://www.openstreetmap.org"
2023-03-28 05:13:48 +02:00
return movedSomething
}
}
2023-04-15 02:28:24 +02:00
2025-02-04 01:06:55 +01:00
class NormalizePanoramax extends SimpleMetaTagger {
constructor ( ) {
2025-02-10 02:04:58 +01:00
super ( {
keys : [ "panoramax" ] ,
doc : "Converts a `panoramax=hash1;hash2;hash3;...` into `panoramax=hash1`,`panoramax:0=hash1`..." ,
isLazy : false ,
cleanupRetagger : true ,
} )
2025-02-04 01:06:55 +01:00
}
2025-02-10 02:04:58 +01:00
private addValue (
comesFromKey : string ,
tags : Record < string , string > ,
hashesToAdd : string [ ] ,
postfix? : string
) {
2025-02-04 01:06:55 +01:00
let basekey = "panoramax"
if ( postfix ) {
basekey = "panoramax:" + postfix
}
let index = - 1
for ( let i = 0 ; i < hashesToAdd . length ; i ++ ) {
let k = basekey
do {
if ( index >= 0 ) {
k = ` ${ basekey } : ${ index } `
}
index ++
} while ( k !== comesFromKey && tags [ k ] )
tags [ k ] = hashesToAdd [ i ]
}
}
/ * *
* const tags = new UIEventSource ( { panoramax : "abc;def;ghi" , "panoramax:2" : "xyz;uvw" , "panoramax:streetsign" : "a;b;c" } )
* const _ = undefined
* new NormalizePanoramax ( ) . applyMetaTagsOnFeature ( _ , _ , tags , _ )
* tags . data // => {"panoramax": "abc", "panoramax:0" : "def", "panoramax:1": "ghi", "panoramax:2":"xyz", "panoramax:3":"uvw", "panoramax:streetsign":"a", "panoramax:streetsign:0":"b","panoramax:streetsign:1": "c"}
* /
2025-02-10 02:04:58 +01:00
applyMetaTagsOnFeature (
feature : Feature ,
layer : LayerConfig ,
tags : UIEventSource < Record < string , string > >
) : boolean {
2025-02-04 01:06:55 +01:00
const tgs = tags . data
let somethingChanged = false
for ( const key in tgs ) {
if ( ! ( key === "panoramax" || key . startsWith ( "panoramax:" ) ) ) {
continue
}
const v = tgs [ key ]
if ( v . indexOf ( ";" ) < 0 ) {
continue
}
const parts = v . split ( ";" )
if ( key === "panoramax" || key . match ( "panoramax:[0-9]+" ) ) {
this . addValue ( key , tgs , parts )
somethingChanged = true
} else {
const postfix = key . match ( /panoramax:([^:]+)(:[0-9]+)?/ ) ? . [ 1 ]
if ( postfix ) {
this . addValue ( key , tgs , parts , postfix )
somethingChanged = true
}
}
}
return somethingChanged
}
}
2023-03-28 05:13:48 +02:00
export default class SimpleMetaTaggers {
/ * *
* A simple metatagger which rewrites various metatags as needed
* /
public static readonly objectMetaInfo = new RewriteMetaInfoTags ( )
2022-01-26 20:47:08 +01:00
public static country = new CountryTagger ( )
2023-03-26 05:58:28 +02:00
public static geometryType = new InlineMetaTagger (
2022-01-26 20:47:08 +01:00
{
keys : [ "_geometry:type" ] ,
2025-02-10 02:04:58 +01:00
doc : "Adds the geometry type as property. This is identical to the GoeJson geometry type and is one of `Point`,`LineString`, `Polygon` and exceptionally `MultiPolygon` or `MultiLineString`" ,
2022-01-26 20:47:08 +01:00
} ,
2024-05-24 15:39:32 +02:00
( feature ) = > {
2022-01-26 20:47:08 +01:00
const changed = feature . properties [ "_geometry:type" ] === feature . geometry . type
feature . properties [ "_geometry:type" ] = feature . geometry . type
return changed
}
)
2023-03-26 05:58:28 +02:00
public static referencingWays = new ReferencingWaysMetaTagger ( )
2025-02-04 01:06:55 +01:00
private static normalizePanoramax = new NormalizePanoramax ( )
2022-01-26 20:47:08 +01:00
private static readonly cardinalDirections = {
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 ,
2025-02-10 02:04:58 +01:00
NNW : 337.5 ,
2022-01-26 20:47:08 +01:00
}
2023-03-26 05:58:28 +02:00
private static latlon = new InlineMetaTagger (
2021-04-25 13:25:03 +02:00
{
keys : [ "_lat" , "_lon" ] ,
2025-02-10 02:04:58 +01:00
doc : "The latitude and longitude of the point (or centerpoint in the case of a way/area)" ,
2021-04-25 13:25:03 +02:00
} ,
2021-03-24 01:25:57 +01:00
( feature ) = > {
const centerPoint = GeoOperations . centerpoint ( feature )
const lat = centerPoint . geometry . coordinates [ 1 ]
const lon = centerPoint . geometry . coordinates [ 0 ]
feature . properties [ "_lat" ] = "" + lat
feature . properties [ "_lon" ] = "" + lon
2021-09-26 17:36:39 +02:00
return true
2021-03-24 01:25:57 +01:00
}
)
2023-03-26 05:58:28 +02:00
private static layerInfo = new InlineMetaTagger (
2021-10-12 02:12:45 +02:00
{
doc : "The layer-id to which this feature belongs. Note that this might be return any applicable if `passAllFeatures` is defined." ,
2021-10-22 01:42:44 +02:00
keys : [ "_layer" ] ,
2025-02-10 02:04:58 +01:00
includesDates : false ,
2021-10-12 02:12:45 +02:00
} ,
2023-03-26 05:58:28 +02:00
( feature , layer ) = > {
2021-10-22 01:42:44 +02:00
if ( feature . properties . _layer === layer . id ) {
2021-10-12 02:12:45 +02:00
return false
}
feature . properties . _layer = layer . id
return true
}
)
2023-03-26 05:58:28 +02:00
private static noBothButLeftRight = new InlineMetaTagger (
2021-10-22 01:42:44 +02:00
{
keys : [
"sidewalk:left" ,
"sidewalk:right" ,
"generic_key:left:property" ,
2025-02-10 02:04:58 +01:00
"generic_key:right:property" ,
2021-10-22 01:42:44 +02:00
] ,
doc : "Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' and 'generic_key:right:property' (and similar for sidewalk tagging). Note that this rewritten tags _will be reuploaded on a change_. To prevent to much unrelated retagging, this is only enabled if the layer has at least some lineRenderings with offset defined" ,
includesDates : false ,
2025-02-10 02:04:58 +01:00
cleanupRetagger : true ,
2021-10-22 01:42:44 +02:00
} ,
2023-03-26 05:58:28 +02:00
( feature , layer ) = > {
2021-11-07 16:34:51 +01:00
if ( ! layer . lineRendering . some ( ( lr ) = > lr . leftRightSensitive ) ) {
2021-10-22 01:42:44 +02:00
return
}
2021-11-07 16:34:51 +01:00
2021-12-07 02:22:56 +01:00
return SimpleMetaTaggers . removeBothTagging ( feature . properties )
2021-10-22 01:42:44 +02:00
}
)
2023-03-26 05:58:28 +02:00
private static surfaceArea = new InlineMetaTagger (
2021-04-25 13:25:03 +02:00
{
2023-06-01 02:52:21 +02:00
keys : [ "_surface" ] ,
doc : "The surface area of the feature in square meters. Not set on points and ways" ,
2025-02-10 02:04:58 +01:00
isLazy : true ,
2021-04-25 13:25:03 +02:00
} ,
2021-03-24 01:25:57 +01:00
( feature ) = > {
2025-02-04 01:06:55 +01:00
if ( feature . geometry . type !== "Polygon" && feature . geometry . type !== "MultiPolygon" ) {
return
}
const f = < Feature < Polygon | MultiPolygon > > feature
2023-06-01 02:52:21 +02:00
Utils . AddLazyProperty ( feature . properties , "_surface" , ( ) = > {
2025-02-04 01:06:55 +01:00
return "" + GeoOperations . surfaceAreaInSqMeters ( f )
2021-10-10 23:38:09 +02:00
} )
2023-06-01 02:52:21 +02:00
return true
}
)
private static surfaceAreaHa = new InlineMetaTagger (
{
keys : [ "_surface:ha" ] ,
doc : "The surface area of the feature in hectare. Not set on points and ways" ,
2025-02-10 02:04:58 +01:00
isLazy : true ,
2023-06-01 02:52:21 +02:00
} ,
( feature ) = > {
2025-02-04 01:06:55 +01:00
if ( feature . geometry . type !== "Polygon" && feature . geometry . type !== "MultiPolygon" ) {
return
}
const f = < Feature < Polygon | MultiPolygon > > feature
2023-03-21 20:59:31 +01:00
Utils . AddLazyProperty ( feature . properties , "_surface:ha" , ( ) = > {
2025-02-04 01:06:55 +01:00
const sqMeters = GeoOperations . surfaceAreaInSqMeters ( f )
2023-03-21 20:59:31 +01:00
return "" + Math . floor ( sqMeters / 1000 ) / 10
2021-10-10 23:38:09 +02:00
} )
2021-10-22 01:42:44 +02:00
2021-09-26 17:36:39 +02:00
return true
2021-03-24 01:25:57 +01:00
}
)
2023-03-26 05:58:28 +02:00
private static levels = new InlineMetaTagger (
2022-07-22 01:33:11 +02:00
{
2023-09-25 19:07:11 +02:00
doc : "Extract the 'level'-tag into a normalized, ';'-separated value called '_level' (which also includes 'repeat_on'). The `level` tag (without underscore) will be normalized with only the value of `level`." ,
2025-02-10 02:04:58 +01:00
keys : [ "_level" ] ,
2022-07-22 01:33:11 +02:00
} ,
( feature ) = > {
2023-09-25 19:07:11 +02:00
let somethingChanged = false
if ( feature . properties [ "level" ] !== undefined ) {
const l = feature . properties [ "level" ]
const newValue = TagUtils . LevelsParser ( l ) . join ( ";" )
if ( l !== newValue ) {
feature . properties [ "level" ] = newValue
somethingChanged = true
}
2022-07-22 01:33:11 +02:00
}
2022-09-08 21:40:48 +02:00
2023-09-25 19:07:11 +02:00
if ( feature . properties [ "repeat_on" ] !== undefined ) {
const l = feature . properties [ "repeat_on" ]
const newValue = TagUtils . LevelsParser ( l ) . join ( ";" )
if ( l !== newValue ) {
feature . properties [ "repeat_on" ] = newValue
somethingChanged = true
}
2022-07-22 01:33:11 +02:00
}
2023-09-25 19:07:11 +02:00
const combined = TagUtils . LevelsParser (
( feature . properties . repeat_on ? ? "" ) + ";" + ( feature . properties . level ? ? "" )
) . join ( ";" )
if ( feature . properties [ "_level" ] !== combined ) {
feature . properties [ "_level" ] = combined
somethingChanged = true
}
return somethingChanged
2022-07-22 01:33:11 +02:00
}
)
2023-03-26 05:58:28 +02:00
private static canonicalize = new InlineMetaTagger (
2021-06-22 00:29:07 +02:00
{
2022-08-18 19:17:15 +02:00
doc : "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`; `1` will be rewritten to `1m` as well)" ,
2025-02-10 02:04:58 +01:00
keys : [ "Theme-defined keys" ] ,
2021-06-22 00:29:07 +02:00
} ,
2023-03-26 05:58:28 +02:00
( feature , _ , __ , state ) = > {
2022-07-22 01:33:11 +02:00
const units = Utils . NoNull (
2024-10-17 04:06:03 +02:00
[ ] . concat ( . . . ( state ? . theme ? . layers ? . map ( ( layer ) = > layer . units ) ? ? [ ] ) )
2022-09-08 21:40:48 +02:00
)
2021-09-21 02:10:42 +02:00
if ( units . length == 0 ) {
2021-09-17 16:54:12 +02:00
return
}
2021-07-13 18:52:02 +02:00
let rewritten = false
2021-06-22 00:29:07 +02:00
for ( const key in feature . properties ) {
for ( const unit of units ) {
2021-09-30 04:13:23 +02:00
if ( unit === undefined ) {
2021-09-27 15:38:12 +02:00
continue
}
2021-09-21 02:10:42 +02:00
if ( unit . appliesToKeys === undefined ) {
2021-09-17 16:54:12 +02:00
console . error ( "The unit " , unit , "has no appliesToKey defined" )
continue
}
2021-06-22 00:29:07 +02:00
if ( ! unit . appliesToKeys . has ( key ) ) {
continue
}
const value = feature . properties [ key ]
2022-08-18 19:17:15 +02:00
const denom = unit . findDenomination ( value , ( ) = > feature . properties [ "_country" ] )
2021-09-30 04:13:23 +02:00
if ( denom === undefined ) {
// no valid value found
break
}
const [ , denomination ] = denom
2022-08-18 19:17:15 +02:00
const defaultDenom = unit . getDefaultDenomination (
( ) = > feature . properties [ "_country" ]
)
2024-05-24 15:39:32 +02:00
const canonical =
2024-06-16 16:06:26 +02:00
denomination ? . canonicalValue (
value ,
defaultDenom == denomination ,
unit . inverted
) ? ? undefined
2021-09-09 00:05:51 +02:00
if ( canonical === value ) {
2021-07-10 21:03:17 +02:00
break
}
console . log ( "Rewritten " , key , ` from ' ${ value } ' into ' ${ canonical } ' ` )
2021-07-24 01:59:57 +02:00
if ( canonical === undefined && ! unit . eraseInvalid ) {
2021-06-22 12:13:44 +02:00
break
}
2021-07-24 01:59:57 +02:00
2021-06-22 03:16:45 +02:00
feature . properties [ key ] = canonical
2021-07-13 18:52:02 +02:00
rewritten = true
2021-06-22 12:13:44 +02:00
break
2021-06-22 00:29:07 +02:00
}
}
2021-09-26 17:36:39 +02:00
return rewritten
2021-06-22 00:29:07 +02:00
}
)
2023-03-26 05:58:28 +02:00
private static lngth = new InlineMetaTagger (
2021-04-25 13:25:03 +02:00
{
keys : [ "_length" , "_length:km" ] ,
2025-02-10 02:04:58 +01:00
doc : "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter" ,
2021-04-25 13:25:03 +02:00
} ,
2021-04-18 14:24:30 +02:00
( feature ) = > {
const l = GeoOperations . lengthInMeters ( feature )
feature . properties [ "_length" ] = "" + l
const km = Math . floor ( l / 1000 )
const kmRest = Math . round ( ( l - km * 1000 ) / 100 )
2021-04-25 13:25:03 +02:00
feature . properties [ "_length:km" ] = "" + km + "." + kmRest
2021-09-26 17:36:39 +02:00
return true
2021-04-18 14:24:30 +02:00
}
)
2023-03-26 05:58:28 +02:00
private static isOpen = new InlineMetaTagger (
2021-04-25 13:25:03 +02:00
{
2022-01-06 18:51:52 +01:00
keys : [ "_isOpen" ] ,
2021-04-25 13:25:03 +02:00
doc : "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')" ,
2021-10-10 23:38:09 +02:00
includesDates : true ,
2025-02-10 02:04:58 +01:00
isLazy : true ,
2021-04-25 13:25:03 +02:00
} ,
2023-03-26 05:58:28 +02:00
( feature ) = > {
2021-04-25 13:25:03 +02:00
if ( Utils . runningFromConsole ) {
2021-04-22 13:30:00 +02:00
// We are running from console, thus probably creating a cache
// isOpen is irrelevant
2021-09-26 17:36:39 +02:00
return false
2021-04-22 13:30:00 +02:00
}
2023-12-31 14:09:25 +01:00
if ( feature . properties . opening_hours === undefined ) {
return false
}
2022-01-26 21:40:38 +01:00
if ( feature . properties . opening_hours === "24/7" ) {
2022-01-26 20:47:08 +01:00
feature . properties . _isOpen = "yes"
return true
}
2022-01-26 21:40:38 +01:00
2022-02-06 03:45:32 +01:00
// _isOpen is calculated dynamically on every call
2021-10-22 01:42:44 +02:00
Object . defineProperty ( feature . properties , "_isOpen" , {
2021-10-10 23:38:09 +02:00
enumerable : false ,
configurable : true ,
get : ( ) = > {
2022-02-06 03:45:32 +01:00
const tags = feature . properties
if ( tags . opening_hours === undefined ) {
return
}
2023-12-31 14:09:25 +01:00
const country = tags . _country
if ( country === undefined ) {
2022-02-06 03:45:32 +01:00
return
2022-01-26 20:47:08 +01:00
}
2021-10-10 23:38:09 +02:00
2022-02-06 03:45:32 +01:00
try {
const [ lon , lat ] = GeoOperations . centerpointCoordinates ( feature )
const oh = new opening_hours (
tags [ "opening_hours" ] ,
{
lat : lat ,
lon : lon ,
address : {
2022-07-07 22:35:28 +02:00
country_code : tags._country.toLowerCase ( ) ,
2025-02-10 02:04:58 +01:00
state : undefined ,
} ,
2022-07-22 01:33:11 +02:00
} ,
2024-06-16 16:06:26 +02:00
< any > { tag_key : "opening_hours" }
2022-09-08 21:40:48 +02:00
)
2021-03-24 01:25:57 +01:00
2022-02-06 03:45:32 +01:00
// Recalculate!
return oh . getState ( ) ? "yes" : "no"
} catch ( e ) {
console . warn ( "Error while parsing opening hours of " , tags . id , e )
delete tags . _isOpen
tags [ "_isOpen" ] = "parse_error"
}
2025-02-10 02:04:58 +01:00
} ,
2022-07-22 01:33:11 +02:00
} )
2021-03-24 01:25:57 +01:00
}
)
2023-03-26 05:58:28 +02:00
private static directionSimplified = new InlineMetaTagger (
2021-04-25 13:25:03 +02:00
{
Fix issues with camera rotation
This commit fixes at least these issues that I was aware of:
* Cardinal directions (e.g. NE) were not recognized.
* The camera icon did not rotatie when direction=* was used instead of
camera:direction, but the blue direction visualizer did.
Pietervdvn said he would have liked to convert the code for direction
normalizing to calculatedTags in a JSON file (as documented in
Docs/CalculatedTags.md), but when he saw the oneliners I had to produce
in response, I was allowed to keep it in SimpleMetaTagger.ts for now.
For your amusement, the oneliners are included below.
"calculatedTags": [
"_direction:numerical=(dir => dir === undefined ? undefined : ({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}[dir] ?? (isNaN(parseFloat(dir)) ? undefined : ((parseFloat(dir) % 360 + 360) % 360)))))(feat.properties['camera:direction'] ?? feat.properties.direction)",
"_direction:leftright=feat.properties['_direction:numerical'] === undefined ? undefined : (feat.properties['_direction:numerical'] <= 180 ? 'right' : 'left')"
]
2021-04-28 16:45:48 +02:00
keys : [ "_direction:numerical" , "_direction:leftright" ] ,
2025-02-10 02:04:58 +01:00
doc : "_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map" ,
2021-04-25 13:25:03 +02:00
} ,
2021-03-24 01:25:57 +01:00
( feature ) = > {
const tags = feature . properties
const direction = tags [ "camera:direction" ] ? ? tags [ "direction" ]
if ( direction === undefined ) {
2021-09-26 17:36:39 +02:00
return false
2021-03-24 01:25:57 +01:00
}
2021-12-07 02:22:56 +01:00
const n = SimpleMetaTaggers . cardinalDirections [ direction ] ? ? Number ( direction )
2021-03-24 01:25:57 +01:00
if ( isNaN ( n ) ) {
2021-09-26 17:36:39 +02:00
return false
2021-03-24 01:25:57 +01:00
}
Fix issues with camera rotation
This commit fixes at least these issues that I was aware of:
* Cardinal directions (e.g. NE) were not recognized.
* The camera icon did not rotatie when direction=* was used instead of
camera:direction, but the blue direction visualizer did.
Pietervdvn said he would have liked to convert the code for direction
normalizing to calculatedTags in a JSON file (as documented in
Docs/CalculatedTags.md), but when he saw the oneliners I had to produce
in response, I was allowed to keep it in SimpleMetaTagger.ts for now.
For your amusement, the oneliners are included below.
"calculatedTags": [
"_direction:numerical=(dir => dir === undefined ? undefined : ({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}[dir] ?? (isNaN(parseFloat(dir)) ? undefined : ((parseFloat(dir) % 360 + 360) % 360)))))(feat.properties['camera:direction'] ?? feat.properties.direction)",
"_direction:leftright=feat.properties['_direction:numerical'] === undefined ? undefined : (feat.properties['_direction:numerical'] <= 180 ? 'right' : 'left')"
]
2021-04-28 16:45:48 +02:00
// The % operator has range (-360, 360). We apply a trick to get [0, 360).
const normalized = ( ( n % 360 ) + 360 ) % 360
2021-03-24 01:25:57 +01:00
Fix issues with camera rotation
This commit fixes at least these issues that I was aware of:
* Cardinal directions (e.g. NE) were not recognized.
* The camera icon did not rotatie when direction=* was used instead of
camera:direction, but the blue direction visualizer did.
Pietervdvn said he would have liked to convert the code for direction
normalizing to calculatedTags in a JSON file (as documented in
Docs/CalculatedTags.md), but when he saw the oneliners I had to produce
in response, I was allowed to keep it in SimpleMetaTagger.ts for now.
For your amusement, the oneliners are included below.
"calculatedTags": [
"_direction:numerical=(dir => dir === undefined ? undefined : ({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}[dir] ?? (isNaN(parseFloat(dir)) ? undefined : ((parseFloat(dir) % 360 + 360) % 360)))))(feat.properties['camera:direction'] ?? feat.properties.direction)",
"_direction:leftright=feat.properties['_direction:numerical'] === undefined ? undefined : (feat.properties['_direction:numerical'] <= 180 ? 'right' : 'left')"
]
2021-04-28 16:45:48 +02:00
tags [ "_direction:numerical" ] = normalized
tags [ "_direction:leftright" ] = normalized <= 180 ? "right" : "left"
2021-09-26 17:36:39 +02:00
return true
2021-03-24 01:25:57 +01:00
}
)
2023-03-26 05:58:28 +02:00
private static directionCenterpoint = new InlineMetaTagger (
2022-09-27 18:52:13 +02:00
{
2022-10-27 01:50:01 +02:00
keys : [ "_direction:centerpoint" ] ,
2022-09-27 18:52:13 +02:00
isLazy : true ,
2025-02-10 02:04:58 +01:00
doc : "_direction:centerpoint is the direction of the linestring (in degrees) if one were standing at the projected centerpoint." ,
2022-09-27 18:52:13 +02:00
} ,
( feature : Feature ) = > {
2022-10-27 01:50:01 +02:00
if ( feature . geometry . type !== "LineString" ) {
2022-09-27 18:52:13 +02:00
return false
}
2022-10-27 01:50:01 +02:00
const ls = < Feature < LineString > > feature
2022-09-27 18:52:13 +02:00
Object . defineProperty ( feature . properties , "_direction:centerpoint" , {
enumerable : false ,
configurable : true ,
get : ( ) = > {
const centroid = GeoOperations . centerpoint ( feature )
2022-10-27 01:50:01 +02:00
const projected = GeoOperations . nearestPoint (
ls ,
< [ number , number ] > centroid . geometry . coordinates
)
2022-09-27 18:52:13 +02:00
const nextPoint = ls . geometry . coordinates [ projected . properties . index + 1 ]
const bearing = GeoOperations . bearing ( projected . geometry . coordinates , nextPoint )
delete feature . properties [ "_direction:centerpoint" ]
feature . properties [ "_direction:centerpoint" ] = bearing
return bearing
2025-02-10 02:04:58 +01:00
} ,
2022-09-27 18:52:13 +02:00
} )
return true
}
)
2023-03-26 05:58:28 +02:00
private static currentTime = new InlineMetaTagger (
2021-04-25 13:25:03 +02:00
{
2023-03-23 01:42:47 +01:00
keys : [ "_now:date" , "_now:datetime" ] ,
2021-04-25 13:25:03 +02:00
doc : "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely" ,
2025-02-10 02:04:58 +01:00
includesDates : true ,
2021-04-25 13:25:03 +02:00
} ,
2023-03-23 01:42:47 +01:00
( feature ) = > {
2021-03-24 01:25:57 +01:00
const now = new Date ( )
function date ( d : Date ) {
return d . toISOString ( ) . slice ( 0 , 10 )
}
function datetime ( d : Date ) {
return d . toISOString ( ) . slice ( 0 , - 5 ) . replace ( "T" , " " )
}
feature . properties [ "_now:date" ] = date ( now )
feature . properties [ "_now:datetime" ] = datetime ( now )
2021-09-26 17:36:39 +02:00
return true
2021-03-24 01:25:57 +01:00
}
2021-12-30 22:01:23 +01:00
)
2023-04-20 17:42:07 +02:00
private static timeSinceLastEdit = new InlineMetaTagger (
{
keys : [ "_last_edit:passed_time" ] ,
doc : "Gives the number of seconds since the last edit. Note that this will _not_ update, but rather be the number of seconds elapsed at the moment this tag is read first" ,
isLazy : true ,
2025-02-10 02:04:58 +01:00
includesDates : true ,
2023-04-20 17:42:07 +02:00
} ,
2024-01-24 23:45:20 +01:00
( feature ) = > {
2023-04-20 17:42:07 +02:00
Utils . AddLazyProperty ( feature . properties , "_last_edit:passed_time" , ( ) = > {
const lastEditTimestamp = new Date (
feature . properties [ "_last_edit:timestamp" ]
) . getTime ( )
const now : number = Date . now ( )
const millisElapsed = now - lastEditTimestamp
return "" + millisElapsed / 1000
} )
return true
}
)
2023-07-17 20:25:19 +02:00
private static currency = new InlineMetaTagger (
{
keys : [ "_currency" ] ,
doc : "Adds the currency valid for the object, based on country or explicit tagging. Can be a single currency or a semicolon-separated list of currencies. Empty if no currency is found." ,
2025-02-10 02:04:58 +01:00
isLazy : true ,
2023-07-17 20:25:19 +02:00
} ,
2023-07-18 01:53:37 +02:00
( feature : Feature , layer : LayerConfig , tagsStore : UIEventSource < OsmTags > ) = > {
2024-07-21 10:52:51 +02:00
if ( tagsStore === undefined ) {
2024-07-17 18:42:39 +02:00
return
}
2023-07-18 01:53:37 +02:00
Utils . AddLazyPropertyAsync ( feature . properties , "_currency" , async ( ) = > {
// Wait until _country is actually set
2023-07-18 12:38:30 +02:00
const tags = await tagsStore . AsPromise ( ( tags ) = > ! ! tags . _country )
2023-07-18 01:53:37 +02:00
2023-07-18 12:38:30 +02:00
const country = tags . _country
2023-07-17 20:25:19 +02:00
// Initialize a list of currencies
const currencies = { }
// Check if there are any currency:XXX tags, add them to the map
for ( const key in feature . properties ) {
2024-05-24 15:39:32 +02:00
if ( ! key . startsWith ( "currency:" ) ) {
continue
2023-07-17 20:25:19 +02:00
}
2024-05-24 15:39:32 +02:00
const currency = key . slice ( 9 )
const hasCurrency = feature . properties [ key ] === "yes"
currencies [ currency ] = hasCurrency
2023-07-17 20:25:19 +02:00
}
// Determine the default currency for the country
2023-07-18 12:38:30 +02:00
const defaultCurrency = countryToCurrency [ country . toUpperCase ( ) ]
2023-07-17 20:25:19 +02:00
// If the default currency is not in the list, add it
if ( defaultCurrency && ! currencies [ defaultCurrency ] ) {
currencies [ defaultCurrency ] = true
}
if ( currencies ) {
return Object . keys ( currencies )
. filter ( ( key ) = > currencies [ key ] )
. join ( ";" )
}
return ""
} )
return true
}
)
2021-12-07 02:22:56 +01:00
public static metatags : SimpleMetaTagger [ ] = [
SimpleMetaTaggers . latlon ,
SimpleMetaTaggers . layerInfo ,
SimpleMetaTaggers . surfaceArea ,
2023-06-01 02:52:21 +02:00
SimpleMetaTaggers . surfaceAreaHa ,
2021-12-07 02:22:56 +01:00
SimpleMetaTaggers . lngth ,
SimpleMetaTaggers . canonicalize ,
SimpleMetaTaggers . country ,
SimpleMetaTaggers . isOpen ,
SimpleMetaTaggers . directionSimplified ,
2022-09-27 18:52:13 +02:00
SimpleMetaTaggers . directionCenterpoint ,
2021-12-07 02:22:56 +01:00
SimpleMetaTaggers . currentTime ,
SimpleMetaTaggers . objectMetaInfo ,
2021-12-30 22:01:23 +01:00
SimpleMetaTaggers . noBothButLeftRight ,
2022-07-22 01:33:11 +02:00
SimpleMetaTaggers . geometryType ,
SimpleMetaTaggers . levels ,
2023-01-17 18:23:08 +01:00
SimpleMetaTaggers . referencingWays ,
2023-04-20 17:42:07 +02:00
SimpleMetaTaggers . timeSinceLastEdit ,
2023-07-17 20:25:19 +02:00
SimpleMetaTaggers . currency ,
2025-02-10 02:04:58 +01:00
SimpleMetaTaggers . normalizePanoramax ,
2021-03-24 01:25:57 +01:00
]
2021-12-07 02:22:56 +01:00
2021-11-07 16:34:51 +01:00
/ * *
* Edits the given object to rewrite 'both' - tagging into a 'left-right' tagging scheme .
* These changes are performed in - place .
*
* Returns 'true' is at least one change has been made
* @param tags
* /
2024-05-24 15:39:32 +02:00
public static removeBothTagging ( tags : Record < string , string | number > ) : boolean {
2021-11-07 16:34:51 +01:00
let somethingChanged = false
/ * *
* Sets the key onto the properties ( but doesn ' t overwrite if already existing )
* /
function set ( k , value ) {
if ( tags [ k ] === undefined || tags [ k ] === "" ) {
tags [ k ] = value
somethingChanged = true
}
}
if ( tags [ "sidewalk" ] ) {
const v = tags [ "sidewalk" ]
switch ( v ) {
case "none" :
case "no" :
set ( "sidewalk:left" , "no" )
set ( "sidewalk:right" , "no" )
break
case "both" :
set ( "sidewalk:left" , "yes" )
set ( "sidewalk:right" , "yes" )
break
case "left" :
set ( "sidewalk:left" , "yes" )
set ( "sidewalk:right" , "no" )
break
case "right" :
set ( "sidewalk:left" , "no" )
set ( "sidewalk:right" , "yes" )
break
default :
set ( "sidewalk:left" , v )
set ( "sidewalk:right" , v )
break
}
delete tags [ "sidewalk" ]
somethingChanged = true
}
const regex = /\([^:]*\):both:\(.*\)/
for ( const key in tags ) {
const v = tags [ key ]
if ( key . endsWith ( ":both" ) ) {
const strippedKey = key . substring ( 0 , key . length - ":both" . length )
set ( strippedKey + ":left" , v )
set ( strippedKey + ":right" , v )
delete tags [ key ]
continue
}
const match = key . match ( regex )
if ( match !== null ) {
const strippedKey = match [ 1 ]
const property = match [ 1 ]
set ( strippedKey + ":left:" + property , v )
set ( strippedKey + ":right:" + property , v )
console . log ( "Left-right rewritten " + key )
delete tags [ key ]
}
}
return somethingChanged
}
2024-07-12 03:17:15 +02:00
public static HelpText ( ) : string {
const subElements : string [ ] = [
[
2021-06-15 00:28:59 +02:00
"Metatags are extra tags available, in order to display more data or to give better questions." ,
2022-06-07 19:48:09 +02:00
"They are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags." ,
2025-02-10 02:04:58 +01:00
"**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object" ,
] . join ( "\n" ) ,
2021-03-24 01:25:57 +01:00
]
2024-07-12 03:17:15 +02:00
subElements . push ( "## Metatags calculated by MapComplete" )
2021-06-15 00:28:59 +02:00
subElements . push (
2024-07-21 10:52:51 +02:00
"The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme"
2022-09-08 21:40:48 +02:00
)
2021-12-07 02:22:56 +01:00
for ( const metatag of SimpleMetaTaggers . metatags ) {
2021-03-24 01:25:57 +01:00
subElements . push (
2024-07-21 10:52:51 +02:00
"### " + metatag . keys . join ( ", " ) ,
2021-10-10 23:38:09 +02:00
metatag . doc ,
metatag . isLazy ? "This is a lazy metatag and is only calculated when needed" : ""
2021-03-24 01:25:57 +01:00
)
}
2024-07-12 03:17:15 +02:00
return subElements . join ( "\n\n" )
2021-03-24 01:25:57 +01:00
}
Fix issues with camera rotation
This commit fixes at least these issues that I was aware of:
* Cardinal directions (e.g. NE) were not recognized.
* The camera icon did not rotatie when direction=* was used instead of
camera:direction, but the blue direction visualizer did.
Pietervdvn said he would have liked to convert the code for direction
normalizing to calculatedTags in a JSON file (as documented in
Docs/CalculatedTags.md), but when he saw the oneliners I had to produce
in response, I was allowed to keep it in SimpleMetaTagger.ts for now.
For your amusement, the oneliners are included below.
"calculatedTags": [
"_direction:numerical=(dir => dir === undefined ? undefined : ({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}[dir] ?? (isNaN(parseFloat(dir)) ? undefined : ((parseFloat(dir) % 360 + 360) % 360)))))(feat.properties['camera:direction'] ?? feat.properties.direction)",
"_direction:leftright=feat.properties['_direction:numerical'] === undefined ? undefined : (feat.properties['_direction:numerical'] <= 180 ? 'right' : 'left')"
]
2021-04-28 16:45:48 +02:00
}