2021-05-11 02:39:51 +02:00
import * as colors from "./assets/colors.json"
2021-05-18 19:48:20 +02:00
2020-07-24 01:12:57 +02:00
export class Utils {
2021-01-06 02:21:50 +01:00
/ * *
* In the 'deploy' - step , some code needs to be run by ts - node .
* However , ts - node crashes when it sees 'document' . When running from console , we flag this and disable all code where document is needed .
* This is a workaround and yet another hack
* /
2021-09-22 05:02:09 +02:00
public static runningFromConsole = typeof window === "undefined" ;
2020-11-17 02:22:48 +01:00
public static readonly assets_path = "./assets/svg/" ;
2021-09-26 17:36:39 +02:00
public static externalDownloadFunction : ( url : string , headers? : any ) = > Promise < any > ;
2021-11-07 16:34:51 +01:00
public static Special_visualizations_tagsToApplyHelpText = ` These can either be a tag to add, such as \` amenity=fast_food \` or can use a substitution, e.g. \` addr:housenumber= $ number \` .
2021-10-31 02:08:39 +01:00
This new point will then have the tags \ ` amenity=fast_food \` and \` addr:housenumber \` with the value that was saved in \` number \` in the original feature.
If a value to substitute is undefined , empty string will be used instead .
This supports multiple values , e . g . \ ` ref= $ source:geometry:type/ $ source:geometry:ref \`
Remark that the syntax is slightly different then expected ; it uses '$' to note a value to copy , followed by a name ( matched with \ ` [a-zA-Z0-9_:]* \` ). Sadly, delimiting with \` {} \` as these already mark the boundaries of the special rendering...
Note that these values can be prepare with javascript in the theme by using a [ calculatedTag ] ( calculatedTags . md # calculating - tags - with - javascript )
`
2022-01-26 21:40:38 +01:00
public static readonly imageExtensions = new Set ( [ "jpg" , "png" , "svg" , "jpeg" , ".gif" ] )
2021-12-09 13:16:40 +01:00
public static readonly special_visualizations_importRequirementDocs = ` #### Importing a dataset into OpenStreetMap: requirements
If you want to import a dataset , make sure that :
1 . The dataset to import has a suitable license
2 . The community has been informed of the import
3 . All other requirements of the [ import guidelines ] ( https : //wiki.openstreetmap.org/wiki/Import/Guidelines) have been followed
There are also some technicalities in your theme to keep in mind :
1 . The new feature will be added and will flow through the program as any other new point as if it came from OSM .
This means that there should be a layer which will match the new tags and which will display it .
2 . The original feature from your geojson layer will gain the tag '_imported=yes' .
This should be used to change the appearance or even to hide it ( eg by changing the icon size to zero )
3 . There should be a way for the theme to detect previously imported points , even after reloading .
A reference number to the original dataset is an excellent way to do this
4 . When importing ways , the theme creator is also responsible of avoiding overlapping ways .
# # # # Disabled in unofficial themes
The import button can be tested in an unofficial theme by adding \ ` test=true \` or \` backend=osm-test \` as [URL-paramter](URL_Parameters.md).
The import button will show up then . If in testmode , you can read the changeset - XML directly in the web console .
In the case that MapComplete is pointed to the testing grounds , the edit will be made on https : //master.apis.dev.openstreetmap.org`
2022-01-07 17:31:39 +01:00
private static knownKeys = [ "addExtraTags" , "and" , "calculatedTags" , "changesetmessage" , "clustering" , "color" , "condition" , "customCss" , "dashArray" , "defaultBackgroundId" , "description" , "descriptionTail" , "doNotDownload" , "enableAddNewPoints" , "enableBackgroundLayerSelection" , "enableGeolocation" , "enableLayers" , "enableMoreQuests" , "enableSearch" , "enableShareScreen" , "enableUserBadge" , "freeform" , "hideFromOverview" , "hideInAnswer" , "icon" , "iconOverlays" , "iconSize" , "id" , "if" , "ifnot" , "isShown" , "key" , "language" , "layers" , "lockLocation" , "maintainer" , "mappings" , "maxzoom" , "maxZoom" , "minNeededElements" , "minzoom" , "multiAnswer" , "name" , "or" , "osmTags" , "passAllFeatures" , "presets" , "question" , "render" , "roaming" , "roamingRenderings" , "rotation" , "shortDescription" , "socialImage" , "source" , "startLat" , "startLon" , "startZoom" , "tagRenderings" , "tags" , "then" , "title" , "titleIcons" , "type" , "version" , "wayHandling" , "widenFactor" , "width" ]
private static extraKeys = [ "nl" , "en" , "fr" , "de" , "pt" , "es" , "name" , "phone" , "email" , "amenity" , "leisure" , "highway" , "building" , "yes" , "no" , "true" , "false" ]
private static injectedDownloads = { }
private static _download_cache = new Map < string , { promise : Promise < any > , timestamp : number } > ( )
2021-12-09 13:16:40 +01:00
/ * *
* Parses the arguments for special visualisations
* /
public static ParseVisArgs ( specs : { name : string , defaultValue? : string } [ ] , args : string [ ] ) : any {
const parsed = { } ;
2022-01-07 17:31:39 +01:00
if ( args . length > specs . length ) {
throw "To much arguments for special visualization: got " + args . join ( "," ) + " but expected only " + args . length + " arguments"
2021-12-09 13:16:40 +01:00
}
2022-01-07 17:31:39 +01:00
for ( let i = 0 ; i < specs . length ; i ++ ) {
2021-12-09 13:16:40 +01:00
const spec = specs [ i ] ;
let arg = args [ i ] ? . trim ( ) ;
2022-01-07 17:31:39 +01:00
if ( arg === undefined || arg === "" ) {
2021-12-09 13:16:40 +01:00
arg = spec . defaultValue
}
2022-01-07 17:31:39 +01:00
parsed [ spec . name ] = arg
2021-12-09 13:16:40 +01:00
}
return parsed ;
}
2020-09-30 22:22:58 +02:00
static EncodeXmlValue ( str ) {
2021-05-18 19:48:20 +02:00
if ( typeof str !== "string" ) {
str = "" + str
2021-05-09 18:56:51 +02:00
}
2021-05-18 19:48:20 +02:00
2020-09-30 22:22:58 +02:00
return str . replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /"/g , '"' )
. replace ( /'/g , ''' )
}
2020-07-24 01:12:57 +02:00
/ * *
* Gives a clean float , or undefined if parsing fails
* @param str
* /
static asFloat ( str ) : number {
if ( str ) {
const i = parseFloat ( str ) ;
if ( isNaN ( i ) ) {
return undefined ;
}
return i ;
}
return undefined ;
}
2020-09-30 22:22:58 +02:00
public static Upper ( str : string ) {
return str . substr ( 0 , 1 ) . toUpperCase ( ) + str . substr ( 1 ) ;
}
2020-10-04 01:04:46 +02:00
public static TwoDigits ( i : number ) {
if ( i < 10 ) {
return "0" + i ;
}
return "" + i ;
}
2022-03-15 13:40:23 +01:00
/ * *
* Converts a number to a string with one number after the comma
2022-03-18 01:21:00 +01:00
*
2022-03-15 13:40:23 +01:00
* Utils . Round ( 15 ) // => "15.0"
* Utils . Round ( 1 ) // => "1.0"
* Utils . Round ( 1.5 ) // => "1.5"
* Utils . Round ( 0.5 ) // => "0.5"
* Utils . Round ( 1.6 ) // => "1.6"
* Utils . Round ( - 15 ) // => "-15.0"
* Utils . Round ( - 1 ) // => "-1.0"
* Utils . Round ( - 1.5 ) // => "-1.5"
* Utils . Round ( - 0.5 ) // => "-0.5"
* Utils . Round ( - 1.6 ) // => "-1.6"
* Utils . Round ( - 1531.63543 ) // =>"-1531.6"
* /
public static Round ( i : number ) : string {
2021-03-09 13:10:48 +01:00
if ( i < 0 ) {
2020-10-30 00:56:46 +01:00
return "-" + Utils . Round ( - i ) ;
}
const j = "" + Math . floor ( i * 10 ) ;
if ( j . length == 1 ) {
return "0." + j ;
}
return j . substr ( 0 , j . length - 1 ) + "." + j . substr ( j . length - 1 , j . length ) ;
}
2020-10-04 01:04:46 +02:00
public static Times ( f : ( ( i : number ) = > string ) , count : number ) : string {
2020-09-30 22:22:58 +02:00
let res = "" ;
for ( let i = 0 ; i < count ; i ++ ) {
2020-10-04 01:04:46 +02:00
res += f ( i ) ;
2020-09-30 22:22:58 +02:00
}
return res ;
2020-07-24 14:46:25 +02:00
}
2020-07-31 04:58:58 +02:00
2021-07-03 22:24:12 +02:00
public static TimesT < T > ( count : number , f : ( ( i : number ) = > T ) ) : T [ ] {
let res : T [ ] = [ ] ;
2021-06-16 14:23:53 +02:00
for ( let i = 0 ; i < count ; i ++ ) {
2021-07-03 22:24:12 +02:00
res . push ( f ( i ) ) ;
2021-06-16 14:23:53 +02:00
}
return res ;
}
2020-08-08 02:16:42 +02:00
public static NoNull < T > ( array : T [ ] ) : T [ ] {
2022-01-18 18:12:24 +01:00
return array ? . filter ( o = > o !== undefined && o !== null )
2020-08-08 02:16:42 +02:00
}
2021-03-09 13:10:48 +01:00
2021-10-18 22:17:15 +02:00
public static Hist ( array : string [ ] ) : Map < string , number > {
2021-10-04 03:12:42 +02:00
const hist = new Map < string , number > ( ) ;
for ( const s of array ) {
hist . set ( s , 1 + ( hist . get ( s ) ? ? 0 ) )
}
return hist ;
}
2021-10-18 22:17:15 +02:00
2021-03-09 13:10:48 +01:00
public static NoEmpty ( array : string [ ] ) : string [ ] {
2020-08-22 13:02:31 +02:00
const ls : string [ ] = [ ] ;
for ( const t of array ) {
if ( t === "" ) {
continue ;
}
ls . push ( t ) ;
}
return ls ;
}
2020-08-08 02:16:42 +02:00
2021-03-09 13:10:48 +01:00
public static EllipsesAfter ( str : string , l : number = 100 ) {
2021-06-15 16:18:58 +02:00
if ( str === undefined || str === null ) {
2020-09-03 03:16:43 +02:00
return undefined ;
}
2021-03-09 13:10:48 +01:00
if ( str . length <= l ) {
2020-08-26 00:21:34 +02:00
return str ;
}
2021-03-09 13:10:48 +01:00
return str . substr ( 0 , l - 3 ) + "..." ;
2020-08-26 00:21:34 +02:00
}
2022-04-01 21:17:27 +02:00
public static FixedLength ( str : string , l : number ) {
str = Utils . EllipsesAfter ( str , l )
while ( str . length < l ) {
str = " " + str
}
return str ;
}
2021-03-09 13:10:48 +01:00
public static Dedup ( arr : string [ ] ) : string [ ] {
if ( arr === undefined ) {
2020-08-26 15:36:04 +02:00
return undefined ;
}
const newArr = [ ] ;
for ( const string of arr ) {
2020-10-02 19:00:24 +02:00
if ( newArr . indexOf ( string ) < 0 ) {
2020-08-26 15:36:04 +02:00
newArr . push ( string ) ;
}
}
return newArr ;
}
2021-09-09 00:05:51 +02:00
2021-12-21 18:35:31 +01:00
public static Dupiclates ( arr : string [ ] ) : string [ ] {
2021-11-08 03:00:58 +01:00
if ( arr === undefined ) {
return undefined ;
}
const newArr = [ ] ;
const seen = new Set < string > ( ) ;
for ( const string of arr ) {
2022-01-07 17:31:39 +01:00
if ( seen . has ( string ) ) {
2021-11-08 03:00:58 +01:00
newArr . push ( string )
}
seen . add ( string )
}
return newArr ;
}
2022-01-07 17:31:39 +01:00
2021-09-09 00:05:51 +02:00
public static Identical < T > ( t1 : T [ ] , t2 : T [ ] , eq ? : ( t : T , t0 : T ) = > boolean ) : boolean {
if ( t1 . length !== t2 . length ) {
2021-07-15 20:47:28 +02:00
return false
}
eq = ( a , b ) = > a === b
2021-09-09 00:05:51 +02:00
for ( let i = 0 ; i < t1 . length ; i ++ ) {
if ( ! eq ( t1 [ i ] , t2 [ i ] ) ) {
2021-07-15 20:47:28 +02:00
return false
}
}
return true ;
}
2021-09-09 00:05:51 +02:00
2020-10-02 19:00:24 +02:00
public static MergeTags ( a : any , b : any ) {
2020-08-27 00:08:00 +02:00
const t = { } ;
for ( const k in a ) {
t [ k ] = a [ k ] ;
}
for ( const k in b ) {
t [ k ] = b [ k ] ;
}
return t ;
}
2020-10-02 19:00:24 +02:00
public static SplitFirst ( a : string , sep : string ) : string [ ] {
2020-08-31 02:59:47 +02:00
const index = a . indexOf ( sep ) ;
2020-10-02 19:00:24 +02:00
if ( index < 0 ) {
2020-08-31 02:59:47 +02:00
return [ a ] ;
}
2020-10-02 19:00:24 +02:00
return [ a . substr ( 0 , index ) , a . substr ( index + sep . length ) ] ;
2020-08-31 02:59:47 +02:00
}
2020-07-31 04:58:58 +02:00
2022-01-07 17:31:39 +01:00
/ * *
* Given a piece of text , will replace any key occuring in 'tags' by the corresponding value
* @param txt
* @param tags
* @param useLang
* @constructor
* /
2021-12-14 17:29:21 +01:00
public static SubstituteKeys ( txt : string | undefined , tags : any , useLang? : string ) : string | undefined {
2021-10-22 01:07:32 +02:00
if ( txt === undefined ) {
return undefined
}
2021-10-18 22:17:15 +02:00
const regex = /.*{([^}]*)}.*/
let match = txt . match ( regex )
while ( match ) {
const key = match [ 1 ]
2021-11-07 18:37:42 +01:00
let v = tags [ key ]
2022-01-07 17:31:39 +01:00
if ( v !== undefined ) {
2021-12-06 03:24:33 +01:00
if ( v [ "toISOString" ] != undefined ) {
// This is a date, probably the timestamp of the object
// @ts-ignore
const date : Date = el ;
v = date . toISOString ( )
}
2022-01-26 21:40:38 +01:00
2022-01-07 17:31:39 +01:00
if ( useLang !== undefined && v ? . translations !== undefined ) {
2021-12-14 17:29:21 +01:00
v = v . translations [ useLang ] ? ? v . translations [ "*" ] ? ? ( v . textFor !== undefined ? v . textFor ( useLang ) : v ) ;
}
2022-01-07 17:31:39 +01:00
if ( v . InnerConstructElement !== undefined ) {
console . warn ( "SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is" , key , "\nThe value is" , v )
v = ( < HTMLElement > v . InnerConstructElement ( ) ) ? . innerText
2021-12-06 03:24:33 +01:00
}
2022-01-07 17:31:39 +01:00
if ( typeof v !== "string" ) {
v = "" + v
2021-11-07 18:37:42 +01:00
}
v = v . replace ( /\n/g , "<br/>" )
2022-01-26 21:40:38 +01:00
} else {
2022-01-17 21:33:03 +01:00
// v === undefined
v = ""
2021-11-07 18:37:42 +01:00
}
2022-01-12 02:31:51 +01:00
txt = txt . replace ( "{" + key + "}" , v )
2021-10-18 22:17:15 +02:00
match = txt . match ( regex )
2021-06-14 02:39:23 +02:00
}
2021-10-22 01:07:32 +02:00
2021-06-14 02:39:23 +02:00
return txt ;
}
2021-03-09 13:10:48 +01:00
public static LoadCustomCss ( location : string ) {
2021-02-20 16:48:42 +01:00
const head = document . getElementsByTagName ( 'head' ) [ 0 ] ;
const link = document . createElement ( 'link' ) ;
2020-11-14 03:26:09 +01:00
link . id = "customCss" ;
link . rel = 'stylesheet' ;
link . type = 'text/css' ;
link . href = location ;
link . media = 'all' ;
head . appendChild ( link ) ;
2021-11-10 18:42:31 +01:00
console . log ( "Added custom css file " , location )
2020-11-14 03:26:09 +01:00
}
2021-03-09 13:10:48 +01:00
2021-06-20 03:06:00 +02:00
/ * *
2021-09-28 17:30:48 +02:00
* Copies all key - value pairs of the source into the target . This will change the target
2021-06-20 03:06:00 +02:00
* If the key starts with a '+' , the values of the list will be appended to the target instead of overwritten
2022-03-18 01:21:00 +01:00
*
2022-03-15 01:42:38 +01:00
* const obj = { someValue : 42 } ;
* const override = { someValue : null } ;
* Utils . Merge ( override , obj ) ;
* obj . someValue // => null
2022-03-18 01:21:00 +01:00
*
2022-03-15 01:42:38 +01:00
* const obj = { someValue : 42 } ;
* const override = { someValue : null } ;
* const returned = Utils . Merge ( override , obj ) ;
* returned == obj // => true
2022-03-18 01:21:00 +01:00
*
2022-03-15 01:42:38 +01:00
* const source = {
* abc : "def" ,
* foo : "bar" ,
* list0 : [ "overwritten" ] ,
* "list1+" : [ "appended" ]
* }
* const target = {
* "xyz" : "omega" ,
* "list0" : [ "should-be-gone" ] ,
* "list1" : [ "should-be-kept" ] ,
* "list2" : [ "should-be-untouched" ]
* }
* const result = Utils . Merge ( source , target )
* result . abc // => "def"
* result . foo // => "bar"
* result . xyz // => "omega"
* result . list0 . length // => 1
* result . list0 [ 0 ] // => "overwritten"
* result . list1 . length // => 2
* result . list1 [ 0 ] // => "should-be-kept"
* result . list1 [ 1 ] // => "appended"
* result . list2 . length // => 1
* result . list2 [ 0 ] // => "should-be-untouched"
2021-06-20 03:06:00 +02:00
* /
2022-03-15 01:42:38 +01:00
static Merge < T , S > ( source : S , target : T ) : ( T & S ) {
2022-02-04 00:44:09 +01:00
if ( target === null ) {
2022-03-18 01:21:00 +01:00
return < T & S > source
2022-02-04 00:44:09 +01:00
}
2022-02-19 02:45:15 +01:00
2021-01-06 02:52:38 +01:00
for ( const key in source ) {
2021-05-27 18:55:12 +02:00
if ( ! source . hasOwnProperty ( key ) ) {
2021-05-19 22:38:05 +02:00
continue
}
2022-02-01 04:14:54 +01:00
if ( key . startsWith ( "=" ) ) {
const trimmedKey = key . substr ( 1 ) ;
target [ trimmedKey ] = source [ key ]
continue
}
2021-06-20 03:06:00 +02:00
if ( key . startsWith ( "+" ) || key . endsWith ( "+" ) ) {
const trimmedKey = key . replace ( "+" , "" ) ;
const sourceV = source [ key ] ;
const targetV = ( target [ trimmedKey ] ? ? [ ] )
let newList : any [ ] ;
if ( key . startsWith ( "+" ) ) {
2022-03-15 01:42:38 +01:00
// @ts-ignore
2021-06-20 03:06:00 +02:00
newList = sourceV . concat ( targetV )
} else {
newList = targetV . concat ( sourceV )
}
target [ trimmedKey ] = newList ;
continue ;
}
2022-02-04 00:44:09 +01:00
const sourceV = source [ key ]
2022-03-15 01:42:38 +01:00
// @ts-ignore
2021-01-06 02:52:38 +01:00
const targetV = target [ key ]
2021-09-07 00:23:00 +02:00
if ( typeof sourceV === "object" ) {
2021-07-27 19:35:43 +02:00
if ( sourceV === null ) {
2022-03-15 01:42:38 +01:00
// @ts-ignore
2021-07-27 19:35:43 +02:00
target [ key ] = null
} else if ( targetV === undefined ) {
2022-03-15 01:42:38 +01:00
// @ts-ignore
2021-01-06 02:52:38 +01:00
target [ key ] = sourceV ;
2021-03-09 13:10:48 +01:00
} else {
2021-01-06 02:52:38 +01:00
Utils . Merge ( sourceV , targetV ) ;
}
2021-03-09 13:10:48 +01:00
} else {
2022-03-15 01:42:38 +01:00
// @ts-ignore
2021-01-06 02:52:38 +01:00
target [ key ] = sourceV ;
}
2021-03-09 13:10:48 +01:00
2021-01-06 02:52:38 +01:00
}
2022-03-15 01:42:38 +01:00
// @ts-ignore
2021-01-06 02:52:38 +01:00
return target ;
}
2021-10-18 22:17:15 +02:00
2022-02-09 22:34:02 +01:00
/ * *
* Walks the specified path into the object till the end .
*
* If a list is encountered , this is tranparently walked recursively on every object .
*
2022-03-29 00:20:10 +02:00
* The leaf objects are replaced in the object itself by the specified function
2022-02-09 22:34:02 +01:00
* /
2022-02-18 23:10:27 +01:00
public static WalkPath ( path : string [ ] , object : any , replaceLeaf : ( ( leaf : any , travelledPath : string [ ] ) = > any ) , travelledPath : string [ ] = [ ] ) {
2022-02-09 22:34:02 +01:00
const head = path [ 0 ]
if ( path . length === 1 ) {
// We have reached the leaf
const leaf = object [ head ] ;
if ( leaf !== undefined ) {
2022-02-19 02:45:15 +01:00
if ( Array . isArray ( leaf ) ) {
2022-02-18 23:10:27 +01:00
object [ head ] = leaf . map ( o = > replaceLeaf ( o , travelledPath ) )
2022-02-19 02:45:15 +01:00
} else {
2022-02-18 23:10:27 +01:00
object [ head ] = replaceLeaf ( leaf , travelledPath )
2022-02-09 22:34:02 +01:00
}
}
return
}
const sub = object [ head ]
if ( sub === undefined ) {
return ;
}
if ( typeof sub !== "object" ) {
return ;
}
if ( Array . isArray ( sub ) ) {
2022-02-19 02:45:15 +01:00
sub . forEach ( ( el , i ) = > Utils . WalkPath ( path . slice ( 1 ) , el , replaceLeaf , [ . . . travelledPath , head , "" + i ] ) )
2022-02-09 22:34:02 +01:00
return ;
}
2022-02-19 02:45:15 +01:00
Utils . WalkPath ( path . slice ( 1 ) , sub , replaceLeaf , [ . . . travelledPath , head ] )
2022-02-09 22:34:02 +01:00
}
/ * *
* Walks the specified path into the object till the end .
* If a list is encountered , this is tranparently walked recursively on every object .
*
* The leaf objects are collected in the list
* /
2022-02-19 02:45:15 +01:00
public static CollectPath ( path : string [ ] , object : any , collectedList : { leaf : any , path : string [ ] } [ ] = [ ] , travelledPath : string [ ] = [ ] ) : { leaf : any , path : string [ ] } [ ] {
2022-02-09 22:34:02 +01:00
if ( object === undefined || object === null ) {
return collectedList ;
}
const head = path [ 0 ]
2022-02-19 02:45:15 +01:00
travelledPath = [ . . . travelledPath , head ]
2022-02-09 22:34:02 +01:00
if ( path . length === 1 ) {
// We have reached the leaf
const leaf = object [ head ] ;
if ( leaf === undefined || leaf === null ) {
return collectedList
2022-02-19 02:45:15 +01:00
}
if ( Array . isArray ( leaf ) ) {
2022-03-18 01:21:00 +01:00
for ( let i = 0 ; i < ( < any [ ] > leaf ) . length ; i ++ ) {
2022-02-19 02:45:15 +01:00
const l = ( < any [ ] > leaf ) [ i ] ;
2022-03-18 01:21:00 +01:00
collectedList . push ( { leaf : l , path : [ . . . travelledPath , "" + i ] } )
2022-02-09 22:34:02 +01:00
}
2022-02-19 02:45:15 +01:00
} else {
collectedList . push ( { leaf , path : travelledPath } )
}
2022-02-09 22:34:02 +01:00
return collectedList
}
const sub = object [ head ]
if ( sub === undefined || sub === null ) {
return collectedList ;
}
if ( Array . isArray ( sub ) ) {
2022-02-19 02:45:15 +01:00
sub . forEach ( ( el , i ) = > Utils . CollectPath ( path . slice ( 1 ) , el , collectedList , [ . . . travelledPath , "" + i ] ) )
2022-02-09 22:34:02 +01:00
return collectedList ;
}
if ( typeof sub !== "object" ) {
return collectedList ;
}
2022-02-19 02:45:15 +01:00
return Utils . CollectPath ( path . slice ( 1 ) , sub , collectedList , travelledPath )
2022-02-09 22:34:02 +01:00
}
/ * *
* Apply a function on every leaf of the JSON ; used to rewrite parts of the JSON
* /
2022-03-08 04:09:03 +01:00
static WalkJson ( json : any , f : ( v : number | string | boolean | undefined ) = > any , isLeaf : ( object ) = > boolean = undefined ) {
2022-01-26 21:40:38 +01:00
if ( json === undefined ) {
2022-01-07 17:31:39 +01:00
return f ( undefined )
}
const jtp = typeof json
2022-03-18 01:21:00 +01:00
if ( isLeaf !== undefined ) {
if ( jtp === "object" ) {
if ( isLeaf ( json ) ) {
2022-03-08 04:09:03 +01:00
return f ( json )
}
} else {
return json
}
2022-03-18 01:21:00 +01:00
} else if ( jtp === "boolean" || jtp === "string" || jtp === "number" ) {
2022-01-07 17:31:39 +01:00
return f ( json )
}
2022-03-08 04:09:03 +01:00
if ( Array . isArray ( json ) ) {
2022-01-26 21:40:38 +01:00
return json . map ( sub = > {
2022-03-08 04:09:03 +01:00
return Utils . WalkJson ( sub , f , isLeaf ) ;
2022-01-07 17:31:39 +01:00
} )
}
const cp = { . . . json }
for ( const key in json ) {
2022-03-08 04:09:03 +01:00
cp [ key ] = Utils . WalkJson ( json [ key ] , f , isLeaf )
2022-01-07 17:31:39 +01:00
}
return cp
}
2022-04-01 12:51:55 +02:00
/ * *
* Walks an object recursively . Will hang on objects with loops
* /
static WalkObject ( json : any , collect : ( v : number | string | boolean | undefined , path : string [ ] ) = > any , isLeaf : ( object ) = > boolean = undefined , path = [ ] ) {
if ( json === undefined ) {
return ;
}
const jtp = typeof json
if ( isLeaf !== undefined ) {
if ( jtp !== "object" ) {
return
}
if ( isLeaf ( json ) ) {
return collect ( json , path )
}
} else if ( jtp === "boolean" || jtp === "string" || jtp === "number" ) {
return collect ( json , path )
}
if ( Array . isArray ( json ) ) {
return json . map ( ( sub , i ) = > {
return Utils . WalkObject ( sub , collect , isLeaf , [ . . . path , i ] ) ;
} )
}
for ( const key in json ) {
Utils . WalkObject ( json [ key ] , collect , isLeaf , [ . . . path , key ] )
}
}
2021-03-09 13:10:48 +01:00
static getOrSetDefault < K , V > ( dict : Map < K , V > , k : K , v : ( ) = > V ) {
let found = dict . get ( k ) ;
if ( found !== undefined ) {
return found ;
}
dict . set ( k , v ( ) ) ;
return dict . get ( k ) ;
}
2021-03-14 20:15:11 +01:00
2022-03-15 01:42:38 +01:00
/ * *
* Tries to minify the given JSON by applying some compression
* /
2021-04-04 03:22:56 +02:00
public static MinifyJSON ( stringified : string ) : string {
stringified = stringified . replace ( /\|/g , "||" ) ;
const keys = Utils . knownKeys . concat ( Utils . extraKeys ) ;
for ( let i = 0 ; i < keys . length ; i ++ ) {
const knownKey = keys [ i ] ;
let code = i ;
if ( i >= 124 ) {
code += 1 ; // Character 127 is our 'escape' character |
}
let replacement = "|" + String . fromCharCode ( code )
stringified = stringified . replace ( new RegExp ( ` \ " ${ knownKey } \ ": ` , "g" ) , replacement ) ;
}
return stringified ;
}
public static UnMinify ( minified : string ) : string {
2021-07-03 22:24:12 +02:00
if ( minified === undefined || minified === null ) {
2021-06-14 02:39:23 +02:00
return undefined ;
}
2021-07-03 22:24:12 +02:00
2021-04-04 03:22:56 +02:00
const parts = minified . split ( "|" ) ;
let result = parts . shift ( ) ;
const keys = Utils . knownKeys . concat ( Utils . extraKeys ) ;
for ( const part of parts ) {
if ( part == "" ) {
// Empty string => this was a || originally
result += "|"
continue
}
const i = part . charCodeAt ( 0 ) ;
result += "\"" + keys [ i ] + "\":" + part . substring ( 1 )
}
return result ;
}
2021-09-22 16:07:56 +02:00
public static injectJsonDownloadForTests ( url : string , data ) {
Utils . injectedDownloads [ url ] = data
}
2021-09-28 17:30:48 +02:00
public static download ( url : string , headers? : any ) : Promise < string > {
2021-07-03 22:24:12 +02:00
if ( this . externalDownloadFunction !== undefined ) {
2021-09-26 17:36:39 +02:00
return this . externalDownloadFunction ( url , headers )
2021-06-22 14:21:32 +02:00
}
2021-09-22 16:07:56 +02:00
return new Promise ( ( resolve , reject ) = > {
const xhr = new XMLHttpRequest ( ) ;
xhr . onload = ( ) = > {
if ( xhr . status == 200 ) {
2021-09-28 17:30:48 +02:00
resolve ( xhr . response )
2021-09-28 18:40:45 +02:00
} else if ( xhr . status === 509 || xhr . status === 429 ) {
reject ( "rate limited" )
2021-09-22 16:07:56 +02:00
} else {
reject ( xhr . statusText )
}
} ;
xhr . open ( 'GET' , url ) ;
2021-09-26 17:36:39 +02:00
if ( headers !== undefined ) {
for ( const key in headers ) {
xhr . setRequestHeader ( key , headers [ key ] )
}
}
2021-09-22 16:07:56 +02:00
xhr . send ( ) ;
2021-09-29 16:55:05 +02:00
xhr . onerror = reject
2021-06-22 14:21:32 +02:00
}
)
}
2021-05-07 13:17:42 +02:00
2021-11-12 04:11:53 +01:00
public static upload ( url : string , data , headers? : any ) : Promise < string > {
return new Promise ( ( resolve , reject ) = > {
const xhr = new XMLHttpRequest ( ) ;
xhr . onload = ( ) = > {
if ( xhr . status == 200 ) {
resolve ( xhr . response )
} else if ( xhr . status === 509 || xhr . status === 429 ) {
reject ( "rate limited" )
} else {
reject ( xhr . statusText )
}
} ;
xhr . open ( 'POST' , url ) ;
if ( headers !== undefined ) {
for ( const key in headers ) {
xhr . setRequestHeader ( key , headers [ key ] )
}
}
xhr . send ( data ) ;
xhr . onerror = reject
}
)
}
2021-10-14 03:46:09 +02:00
public static async downloadJsonCached ( url : string , maxCacheTimeMs : number , headers? : any ) : Promise < any > {
const cached = Utils . _download_cache . get ( url )
2021-10-18 22:17:15 +02:00
if ( cached !== undefined ) {
if ( ( new Date ( ) . getTime ( ) - cached . timestamp ) <= maxCacheTimeMs ) {
2021-10-14 03:46:09 +02:00
return cached . promise
}
}
2021-10-20 00:19:33 +02:00
const promise = /*NO AWAIT as we work with the promise directly */ Utils . downloadJson ( url , headers )
2021-10-14 03:46:09 +02:00
Utils . _download_cache . set ( url , { promise , timestamp : new Date ( ) . getTime ( ) } )
return await promise
}
2021-09-28 17:30:48 +02:00
public static async downloadJson ( url : string , headers? : any ) : Promise < any > {
2021-09-28 18:40:45 +02:00
const injected = Utils . injectedDownloads [ url ]
if ( injected !== undefined ) {
console . log ( "Using injected resource for test for URL" , url )
return new Promise ( ( resolve , _ ) = > resolve ( injected ) )
}
2021-09-28 17:30:48 +02:00
const data = await Utils . download ( url , Utils . Merge ( { "accept" : "application/json" } , headers ? ? { } ) )
2021-09-28 18:40:45 +02:00
try {
2022-01-26 21:40:38 +01:00
if ( typeof data === "string" ) {
2022-01-25 18:20:15 +01:00
return JSON . parse ( data )
}
return data
2021-09-28 18:40:45 +02:00
} catch ( e ) {
console . error ( "Could not parse " , data , "due to" , e , "\n" , e . stack )
throw e ;
}
2021-09-28 17:30:48 +02:00
}
2021-05-10 23:46:19 +02:00
/ * *
* Triggers a 'download file' popup which will download the contents
* /
2021-07-27 19:59:41 +02:00
public static offerContentsAsDownloadableFile ( contents : string | Blob , fileName : string = "download.txt" ,
2022-03-18 01:21:00 +01:00
options ? : { mimetype : string | "text/plain" | "text/csv" | "application/vnd.geo+json" | "{gpx=application/gpx+xml}" } ) {
2021-05-07 13:17:42 +02:00
const element = document . createElement ( "a" ) ;
2021-07-22 14:33:49 +02:00
let file ;
2021-07-27 19:35:43 +02:00
if ( typeof ( contents ) === "string" ) {
2021-07-27 19:59:41 +02:00
file = new Blob ( [ contents ] , { type : options ? . mimetype ? ? 'text/plain' } ) ;
2021-07-27 19:35:43 +02:00
} else {
file = contents ;
}
2021-05-07 13:17:42 +02:00
element . href = URL . createObjectURL ( file ) ;
element . download = fileName ;
document . body . appendChild ( element ) ; // Required for this to work in FireFox
element . click ( ) ;
}
2021-05-18 19:48:20 +02:00
public static ColourNameToHex ( color : string ) : string {
2021-05-11 02:39:51 +02:00
return colors [ color . toLowerCase ( ) ] ? ? color ;
}
2021-05-18 19:48:20 +02:00
public static HexToColourName ( hex : string ) : string {
2021-05-11 02:39:51 +02:00
hex = hex . toLowerCase ( )
2021-05-18 19:48:20 +02:00
if ( ! hex . startsWith ( "#" ) ) {
2021-05-11 02:39:51 +02:00
return hex ;
}
const c = Utils . color ( hex ) ;
2021-05-18 19:48:20 +02:00
2021-05-11 02:39:51 +02:00
let smallestDiff = Number . MAX_VALUE ;
let bestColor = undefined ;
for ( const color in colors ) {
2021-05-18 19:48:20 +02:00
if ( ! colors . hasOwnProperty ( color ) ) {
2021-05-11 02:39:51 +02:00
continue ;
}
const foundhex = colors [ color ] ;
2021-05-18 19:48:20 +02:00
if ( typeof foundhex !== "string" ) {
2021-05-11 02:39:51 +02:00
continue
}
2021-05-18 19:48:20 +02:00
if ( foundhex === hex ) {
2021-05-11 02:39:51 +02:00
return color
}
const diff = this . colorDiff ( Utils . color ( foundhex ) , c )
2021-05-18 19:48:20 +02:00
if ( diff > 50 ) {
2021-05-11 02:39:51 +02:00
continue ;
}
2021-05-18 19:48:20 +02:00
if ( diff < smallestDiff ) {
2021-05-11 02:39:51 +02:00
smallestDiff = diff ;
bestColor = color ;
}
}
return bestColor ? ? hex ;
}
2021-05-19 22:38:05 +02:00
2022-03-14 22:57:01 +01:00
/ * *
* Reorders an object : creates a new object where the keys have been added alphabetically
2022-03-18 01:21:00 +01:00
*
2022-03-14 22:57:01 +01:00
* const sorted = Utils . sortKeys ( { x : 'x' , abc : { 'x' : 'x' , 'a' : 'a' } , def : 'def' } )
* JSON . stringify ( sorted ) // => '{"abc":{"a":"a","x":"x"},"def":"def","x":"x"}'
* /
2021-09-14 18:20:25 +02:00
static sortKeys ( o : any ) {
2021-09-22 16:07:56 +02:00
const copy = { }
2021-09-14 18:20:25 +02:00
let keys = Object . keys ( o )
keys = keys . sort ( )
for ( const key of keys ) {
let v = o [ key ]
2021-09-22 16:07:56 +02:00
if ( typeof v === "object" ) {
2021-09-14 18:20:25 +02:00
v = Utils . sortKeys ( v )
}
copy [ key ] = v
}
return copy
}
2021-09-26 17:36:39 +02:00
public static async waitFor ( timeMillis : number ) : Promise < void > {
return new Promise ( ( resolve ) = > {
window . setTimeout ( resolve , timeMillis ) ;
} )
}
2021-10-18 22:17:15 +02:00
public static toHumanTime ( seconds ) : string {
2021-10-13 01:28:46 +02:00
seconds = Math . floor ( seconds )
let minutes = Math . floor ( seconds / 60 )
seconds = seconds % 60
let hours = Math . floor ( minutes / 60 )
minutes = minutes % 60
let days = Math . floor ( hours / 24 )
hours = hours % 24
2021-10-18 22:17:15 +02:00
if ( days > 0 ) {
return days + "days" + " " + hours + "h"
2021-10-13 01:28:46 +02:00
}
2021-10-18 22:17:15 +02:00
return hours + ":" + Utils . TwoDigits ( minutes ) + ":" + Utils . TwoDigits ( seconds )
2021-10-13 01:28:46 +02:00
}
2021-10-18 22:17:15 +02:00
public static DisableLongPresses() {
2021-10-15 05:20:02 +02:00
// Remove all context event listeners on mobile to prevent long presses
window . addEventListener ( 'contextmenu' , ( e ) = > { // Not compatible with IE < 9
if ( e . target [ "nodeName" ] === "INPUT" ) {
return ;
}
e . preventDefault ( ) ;
return false ;
} , false ) ;
}
2021-10-18 22:17:15 +02:00
public static OsmChaLinkFor ( daysInThePast , theme = undefined ) : string {
2021-10-16 18:30:24 +02:00
const now = new Date ( )
const lastWeek = new Date ( now . getTime ( ) - daysInThePast * 24 * 60 * 60 * 1000 )
const date = lastWeek . getFullYear ( ) + "-" + Utils . TwoDigits ( lastWeek . getMonth ( ) + 1 ) + "-" + Utils . TwoDigits ( lastWeek . getDate ( ) )
2021-10-30 01:59:17 +02:00
let osmcha_link = ` "date__gte":[{"label":" ${ date } ","value":" ${ date } "}],"editor":[{"label":"mapcomplete","value":"mapcomplete"}] `
2021-10-18 22:17:15 +02:00
if ( theme !== undefined ) {
2021-10-30 01:59:17 +02:00
osmcha_link = osmcha_link + "," + ` "comment":[{"label":"# ${ theme } ","value":"# ${ theme } "}] `
2021-10-16 18:30:24 +02:00
}
2021-11-07 16:34:51 +01:00
return "https://osmcha.org/?filters=" + encodeURIComponent ( "{" + osmcha_link + "}" )
2021-10-18 22:17:15 +02:00
}
2022-01-07 17:31:39 +01:00
/ * *
* Deepclone an object by serializing and deserializing it
* @param x
* @constructor
* /
static Clone < T > ( x : T ) : T {
if ( x === undefined ) {
return undefined ;
}
return JSON . parse ( JSON . stringify ( x ) ) ;
}
2022-01-26 21:40:38 +01:00
public static ParseDate ( str : string ) : Date {
2022-01-24 03:09:21 +01:00
if ( str . endsWith ( " UTC" ) ) {
str = str . replace ( " UTC" , "+00" )
}
return new Date ( str )
}
2022-03-29 00:20:10 +02:00
public static sortedByLevenshteinDistance < T > ( reference : string , ts : T [ ] , getName : ( t :T ) = > string ) : T [ ] {
const withDistance : [ T , number ] [ ] = ts . map ( t = > [ t , Utils . levenshteinDistance ( getName ( t ) , reference ) ] )
withDistance . sort ( ( [ _ , a ] , [ __ , b ] ) = > a - b )
return withDistance . map ( n = > n [ 0 ] )
}
2022-01-07 17:31:39 +01:00
2022-02-19 02:45:15 +01:00
public static levenshteinDistance ( str1 : string , str2 : string ) {
const track = Array ( str2 . length + 1 ) . fill ( null ) . map ( ( ) = >
Array ( str1 . length + 1 ) . fill ( null ) ) ;
for ( let i = 0 ; i <= str1 . length ; i += 1 ) {
track [ 0 ] [ i ] = i ;
}
for ( let j = 0 ; j <= str2 . length ; j += 1 ) {
track [ j ] [ 0 ] = j ;
}
for ( let j = 1 ; j <= str2 . length ; j += 1 ) {
for ( let i = 1 ; i <= str1 . length ; i += 1 ) {
const indicator = str1 [ i - 1 ] === str2 [ j - 1 ] ? 0 : 1 ;
track [ j ] [ i ] = Math . min (
track [ j ] [ i - 1 ] + 1 , // deletion
track [ j - 1 ] [ i ] + 1 , // insertion
track [ j - 1 ] [ i - 1 ] + indicator , // substitution
) ;
}
}
return track [ str2 . length ] [ str1 . length ] ;
}
2022-03-18 01:21:00 +01:00
public static MapToObj < T > ( d : Map < string , T > , onValue : ( ( t : T , key : string ) = > any ) = undefined ) : object {
const o = { }
d . forEach ( ( value , key ) = > {
if ( onValue !== undefined ) {
value = onValue ( value , key )
}
o [ key ] = value ;
} )
return o
}
2021-10-18 22:17:15 +02:00
private static colorDiff ( c0 : { r : number , g : number , b : number } , c1 : { r : number , g : number , b : number } ) {
return Math . abs ( c0 . r - c1 . r ) + Math . abs ( c0 . g - c1 . g ) + Math . abs ( c0 . b - c1 . b ) ;
}
2022-03-18 01:21:00 +01:00
/ * *
* Utils . colorAsHex ( { r : 255 , g : 128 , b : 0 } ) // => "#ff8000"
* Utils . colorAsHex ( undefined ) // => undefined
* /
public static colorAsHex ( c : { r : number , g : number , b : number } ) {
if ( c === undefined ) {
return undefined
}
function componentToHex ( n ) {
let hex = n . toString ( 16 ) ;
return hex . length == 1 ? "0" + hex : hex ;
2021-10-18 22:17:15 +02:00
}
2022-03-18 01:21:00 +01:00
return "#" + componentToHex ( c . r ) + componentToHex ( c . g ) + componentToHex ( c . b ) ;
}
/ * *
*
* Utils . color ( "#ff8000" ) // => {r: 255, g:128, b: 0}
* Utils . color ( " rgba (12,34,56) " ) // => {r: 12, g:34, b: 56}
* Utils . color ( " rgba (12,34,56,0.5) " ) // => {r: 12, g:34, b: 56}
* Utils . color ( undefined ) // => undefined
* /
public static color ( hex : string ) : { r : number , g : number , b : number } {
if ( hex === undefined ) {
return undefined
}
hex = hex . replace ( /[ \t]/g , "" )
if ( hex . startsWith ( "rgba(" ) ) {
const match = hex . match ( /rgba\(([0-9.]+),([0-9.]+),([0-9.]+)(,[0-9.]*)?\)/ )
if ( match == undefined ) {
return undefined
}
return { r : Number ( match [ 1 ] ) , g : Number ( match [ 2 ] ) , b :Number ( match [ 3 ] ) }
}
2021-10-18 22:17:15 +02:00
if ( ! hex . startsWith ( "#" ) ) {
return undefined ;
}
if ( hex . length === 4 ) {
return {
r : parseInt ( hex . substr ( 1 , 1 ) , 16 ) ,
g : parseInt ( hex . substr ( 2 , 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 ) ,
}
2021-10-16 18:30:24 +02:00
}
2022-03-23 19:48:06 +01:00
public static asDict ( tags : { key : string , value : string | number } [ ] ) : Map < string , string | number > {
const d = new Map < string , string | number > ( )
2022-03-18 01:21:00 +01:00
2022-03-23 19:48:06 +01:00
for ( const tag of tags ) {
d . set ( tag . key , tag . value )
}
2022-03-18 01:21:00 +01:00
2022-03-23 19:48:06 +01:00
return d
}
2021-05-06 03:03:54 +02:00
}
2021-05-18 19:48:20 +02:00