2021-05-11 02:39:51 +02:00
import * as colors from "./assets/colors.json"
2022-12-15 20:24:53 +01: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/"
2022-05-26 13:23:25 +02:00
public static externalDownloadFunction : (
url : string ,
headers? : any
) = > Promise < { content : string } | { redirect : string } >
2022-09-14 12:18:51 +02: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 \` .
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.
2021-10-31 02:08:39 +01:00
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
2022-09-14 12:18:51 +02:00
4 . When importing ways , the theme creator is also responsible of avoiding overlapping ways .
2021-12-09 13:16:40 +01:00
# # # # Disabled in unofficial themes
2022-09-14 12:18:51 +02:00
The import button can be tested in an unofficial theme by adding \ ` test=true \` or \` backend=osm-test \` as [URL-paramter](URL_Parameters.md).
2021-12-09 13:16:40 +01:00
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" ,
2022-09-08 21:40:48 +02:00
]
2022-01-07 17:31:39 +01:00
private static injectedDownloads = { }
2022-12-16 13:45:07 +01:00
private static _download_cache = new Map <
string ,
{
promise : Promise < any | { error : string ; url : string ; statuscode ? : number } >
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"
2022-09-08 21:40:48 +02:00
)
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
}
2022-08-17 02:44:09 +02:00
public static NoNull < T > ( array : T [ ] ) : NonNullable < T > [ ] {
return < any > 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-06 03:06:50 +02:00
2022-04-01 21:17:27 +02:00
public static FixedLength ( str : string , l : number ) {
str = Utils . EllipsesAfter ( str , l )
2022-04-06 03:06:50 +02:00
while ( str . length < l ) {
str = " " + str
2022-04-01 21:17:27 +02:00
}
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
2022-04-06 03:06:50 +02:00
/ * *
* In the given list , all values which are lists will be merged with the values , e . g .
*
2022-04-06 03:11:26 +02:00
* Utils . Flatten ( [ [ 1 , 2 ] , 3 , [ 4 , [ 5 , 6 ] ] ] ) // => [1, 2, 3, 4, [5, 6]]
2022-04-06 03:06:50 +02:00
* /
public static Flatten < T > ( list : ( T | T [ ] ) [ ] ) : T [ ] {
const result = [ ]
for ( const value of list ) {
if ( Array . isArray ( value ) ) {
result . push ( . . . value )
} else {
result . push ( value )
}
}
return result
}
2022-04-06 03:18:17 +02:00
/ * *
* Utils . Identical ( [ 1 , 2 ] , [ 1 , 2 ] ) // => true
* Utils . Identical ( [ 1 , 2 , 3 ] , [ 1 , 2 , 4 } ] ) // => false
* Utils . Identical ( [ 1 , 2 ] , [ 1 , 2 , 3 ] ) // => false
* /
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
2022-04-06 03:18:17 +02:00
/ * *
* Utils . MergeTags ( { k0 : "v0" , "common" : "0" } , { k1 : "v1" , common : "1" } ) // => {k0: "v0", k1:"v1", common: "1"}
* /
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
* /
2022-09-02 12:35:23 +02:00
public static SubstituteKeys (
txt : string | undefined ,
tags? : Record < string , 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 ]
2022-07-08 03:14:55 +02:00
let v = tags === undefined ? undefined : tags [ key ]
2022-10-28 04:33:05 +02:00
if ( v !== undefined && v !== null ) {
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
)
2022-06-28 03:21:18 +02:00
v = ( < HTMLElement > v . InnerConstructElement ( ) ) ? . textContent
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
2023-01-17 02:54:33 +01:00
public static PushList < T > ( target : T [ ] , source? : T [ ] ) {
if ( source === undefined ) {
return
}
target . push ( . . . source )
}
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
2023-01-20 11:59:41 +01:00
* If the key starts with ` = ` , the property will be overwritten .
*
* 'Source' will not be modified , but 'Target' will be
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
* /
2023-01-20 11:59:41 +01:00
static Merge < T , S > ( source : Readonly < 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
}
}
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 .
*
2022-12-13 11:32:47 +01:00
* If a list is encountered , this is transparently walked recursively on every object .
* If 'null' or 'undefined' is encountered , this method stops
2022-02-09 22:34:02 +01:00
*
2022-12-13 11:32:47 +01:00
* The leaf objects are replaced in the object itself by the specified function .
2022-02-09 22:34:02 +01:00
* /
2022-06-21 16:47:54 +02:00
public static WalkPath (
path : string [ ] ,
object : any ,
replaceLeaf : ( leaf : any , travelledPath : string [ ] ) = > any ,
travelledPath : string [ ] = [ ]
) : void {
if ( object == null ) {
return
}
2022-09-08 21:40:48 +02:00
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-12-16 13:45:07 +01:00
if ( object [ head ] === undefined ) {
2022-12-13 11:32:47 +01:00
delete object [ head ]
}
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-09-08 21:40:48 +02:00
)
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-09-08 21:40:48 +02:00
)
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
}
/ * *
2022-04-06 17:28:51 +02:00
* Apply a function on every leaf of the JSON ; used to rewrite parts of the JSON .
* Returns a modified copy of the original object .
2022-05-26 13:23:25 +02:00
*
2022-04-13 00:31:13 +02:00
* 'null' and 'undefined' are _always_ considered a leaf , even if 'isLeaf' says it isn ' t
2022-05-26 13:23:25 +02:00
*
2022-04-06 17:28:51 +02:00
* Hangs if the object contains a loop
2022-05-26 13:23:25 +02:00
*
2022-04-13 00:31:13 +02:00
* // should walk a json
* const walked = Utils . WalkJson ( {
* key : "value"
* } , ( x : string ) = > x + "!" )
* walked // => {key: "value!"}
2022-05-26 13:23:25 +02:00
*
2022-04-13 00:31:13 +02:00
* // should preserve undefined and null:
* const walked = Utils . WalkJson ( {
* u : undefined ,
* n : null ,
* v : "value"
* } , ( x ) = > { if ( x !== undefined && x !== null ) { return x + " ! } ; return x } )
* walked // => {v: "value!", u: undefined, n: null}
2022-05-26 13:23:25 +02:00
*
2022-04-13 00:31:13 +02:00
* // should preserve undefined and null, also with a negative isLeaf:
* const walked = Utils . WalkJson ( {
* u : undefined ,
* n : null ,
* v : "value"
* } , ( x ) = > return x } , _ = > false )
* walked // => {v: "value", u: undefined, n: null}
2022-02-09 22:34:02 +01:00
* /
2022-04-06 17:28:51 +02:00
static WalkJson (
json : any ,
f : ( v : object | number | string | boolean | undefined , path : string [ ] ) = > any ,
isLeaf : ( object ) = > boolean = undefined ,
path : string [ ] = [ ]
) {
2022-04-13 00:31:13 +02:00
if ( json === undefined || json === null ) {
return f ( json , path )
2022-01-07 17:31:39 +01:00
}
const jtp = typeof json
2022-03-18 01:21:00 +01:00
if ( isLeaf !== undefined ) {
if ( jtp === "object" ) {
if ( isLeaf ( json ) ) {
2022-04-06 17:28:51 +02:00
return f ( json , path )
2022-03-08 04:09:03 +01:00
}
} else {
return json
}
2022-03-18 01:21:00 +01:00
} else if ( jtp === "boolean" || jtp === "string" || jtp === "number" ) {
2022-04-06 17:28:51 +02:00
return f ( json , path )
2022-01-07 17:31:39 +01:00
}
2022-03-08 04:09:03 +01:00
if ( Array . isArray ( json ) ) {
2022-05-26 13:23:25 +02:00
return json . map ( ( sub , i ) = > {
return Utils . WalkJson ( sub , f , isLeaf , [ . . . path , "" + i ] )
2022-01-07 17:31:39 +01:00
} )
}
const cp = { . . . json }
for ( const key in json ) {
2022-04-06 17:28:51 +02:00
cp [ key ] = Utils . WalkJson ( json [ key ] , f , isLeaf , [ . . . path , key ] )
2022-01-07 17:31:39 +01:00
}
return cp
}
2022-04-01 12:51:55 +02:00
/ * *
2022-04-06 17:28:51 +02:00
* Walks an object recursively , will execute the 'collect' - callback on every leaf .
2022-05-26 13:23:25 +02:00
*
2022-04-06 17:28:51 +02:00
* Will hang on objects with loops
2022-04-01 12:51:55 +02:00
* /
2022-04-06 17:28:51 +02:00
static WalkObject (
json : any ,
collect : ( v : number | string | boolean | undefined , path : string [ ] ) = > any ,
isLeaf : ( object ) = > boolean = undefined ,
path = [ ]
) : void {
2022-04-01 12:51:55 +02:00
if ( json === undefined ) {
return
}
const jtp = typeof json
if ( isLeaf !== undefined ) {
if ( jtp !== "object" ) {
return
}
2022-04-06 03:06:50 +02:00
2022-04-01 12:51:55 +02:00
if ( isLeaf ( json ) ) {
return collect ( json , path )
}
} else if ( jtp === "boolean" || jtp === "string" || jtp === "number" ) {
2022-04-06 17:28:51 +02:00
collect ( json , path )
return
2022-04-01 12:51:55 +02:00
}
if ( Array . isArray ( json ) ) {
2022-04-06 17:28:51 +02:00
json . map ( ( sub , i ) = > {
2022-04-06 03:06:50 +02:00
return Utils . WalkObject ( sub , collect , isLeaf , [ . . . path , i ] )
2022-04-01 12:51:55 +02:00
} )
2022-04-06 17:28:51 +02:00
return
2022-04-01 12:51:55 +02:00
}
for ( const key in json ) {
2022-04-06 03:06:50 +02:00
Utils . WalkObject ( json [ key ] , collect , isLeaf , [ . . . path , key ] )
2022-04-01 12:51:55 +02:00
}
}
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
}
2022-05-26 13:23:25 +02:00
public static async download ( url : string , headers? : any ) : Promise < string | undefined > {
2022-12-16 01:00:43 +01:00
const result = await Utils . downloadAdvanced ( url , headers )
2022-12-16 13:45:07 +01:00
if ( result [ "error" ] !== undefined ) {
2022-12-16 01:00:43 +01:00
throw result [ "error" ]
}
return result [ "content" ]
2022-05-26 13:23:25 +02:00
}
/ * *
* Download function which also indicates advanced options , such as redirects
* @param url
* @param headers
* /
public static downloadAdvanced (
url : string ,
2022-12-16 13:45:07 +01:00
headers? : any
) : Promise <
| { content : string }
| { redirect : string }
| { error : string ; url : string ; statuscode? : number }
> {
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 ) {
2022-05-26 13:23:25 +02:00
resolve ( { content : xhr.response } )
} else if ( xhr . status === 302 ) {
resolve ( { redirect : xhr.getResponseHeader ( "location" ) } )
2021-09-28 18:40:45 +02:00
} else if ( xhr . status === 509 || xhr . status === 429 ) {
2022-12-16 13:45:07 +01:00
resolve ( { error : "rate limited" , url , statuscode : xhr.status } )
2021-09-22 16:07:56 +02:00
} else {
2022-12-16 13:45:07 +01:00
resolve ( {
error : "other error: " + xhr . statusText ,
url ,
statuscode : xhr.status ,
} )
2021-09-26 17:36:39 +02:00
}
2021-06-22 14:21:32 +02:00
}
xhr . open ( "GET" , url )
2021-11-12 04:11:53 +01:00
if ( headers !== undefined ) {
for ( const key in headers ) {
xhr . setRequestHeader ( key , headers [ key ] )
2022-09-08 21:40:48 +02:00
}
}
2021-09-22 16:07:56 +02:00
xhr . send ( )
2021-11-12 04:11:53 +01:00
xhr . onerror = reject
2022-09-08 21:40:48 +02:00
} )
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
2022-09-08 21:40:48 +02:00
} )
}
2021-11-12 04:11:53 +01:00
2021-10-14 03:46:09 +02:00
public static async downloadJsonCached (
url : string ,
maxCacheTimeMs : number ,
headers? : any
) : Promise < any > {
2022-12-15 20:24:53 +01:00
const result = await Utils . downloadJsonAdvanced ( url , headers )
2022-12-16 13:45:07 +01:00
if ( result [ "content" ] ) {
2022-12-15 20:24:53 +01:00
return result [ "content" ]
}
throw result [ "error" ]
}
public static async downloadJsonCachedAdvanced (
url : string ,
maxCacheTimeMs : number ,
headers? : any
2023-01-04 17:41:11 +01:00
) : Promise < { content : any } | { error : string ; url : string ; statuscode? : number } > {
2021-10-14 03:46:09 +02:00
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
}
}
2022-12-16 13:45:07 +01:00
const promise =
/*NO AWAIT as we work with the promise directly */ Utils . downloadJsonAdvanced (
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 > {
2022-12-15 20:24:53 +01:00
const result = await Utils . downloadJsonAdvanced ( url , headers )
2022-12-16 13:45:07 +01:00
if ( result [ "content" ] ) {
2022-12-15 20:24:53 +01:00
return result [ "content" ]
}
throw result [ "error" ]
}
2022-12-16 13:45:07 +01:00
public static async downloadJsonAdvanced (
url : string ,
headers? : any
) : Promise < { content : any } | { error : string ; url : string ; statuscode? : number } > {
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 )
2022-12-16 13:45:07 +01:00
return new Promise ( ( resolve , _ ) = > resolve ( { content : injected } ) )
2021-09-28 18:40:45 +02:00
}
2022-12-15 20:24:53 +01:00
const result = await Utils . downloadAdvanced (
2021-09-28 17:30:48 +02:00
url ,
Utils . Merge ( { accept : "application/json" } , headers ? ? { } )
2022-09-08 21:40:48 +02:00
)
2022-12-16 13:45:07 +01:00
if ( result [ "error" ] !== undefined ) {
return < { error : string ; url : string ; statuscode? : number } > result
2022-12-15 20:24:53 +01:00
}
const data = result [ "content" ]
2021-09-28 18:40:45 +02:00
try {
2022-01-26 21:40:38 +01:00
if ( typeof data === "string" ) {
2022-12-16 13:45:07 +01:00
return { content : JSON.parse ( data ) }
2022-01-25 18:20:15 +01:00
}
2022-12-16 13:45:07 +01:00
return { content : data }
2021-09-28 18:40:45 +02:00
} catch ( e ) {
console . error ( "Could not parse " , data , "due to" , e , "\n" , e . stack )
2022-12-16 13:45:07 +01:00
return { error : "malformed" , url }
2021-09-28 18:40:45 +02:00
}
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-04-18 02:39:30 +02:00
options ? : {
mimetype :
| string
| "text/plain"
| "text/csv"
| "application/vnd.geo+json"
| "{gpx=application/gpx+xml}"
| "application/json"
}
) {
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" ) {
2022-09-08 21:40:48 +02:00
return
}
2021-10-15 05:20:02 +02:00
e . preventDefault ( )
2021-07-15 20:47:28 +02:00
return false
2022-09-08 21:40:48 +02:00
} ,
false
)
2021-10-15 05:20:02 +02:00
}
2021-10-18 22:17:15 +02:00
2023-01-04 17:41:11 +01:00
public static preventDefaultOnMouseEvent ( event : any ) {
event ? . originalEvent ? . preventDefault ( )
event ? . originalEvent ? . stopPropagation ( )
event ? . originalEvent ? . stopImmediatePropagation ( )
if ( event ? . originalEvent ) {
// This is a total workaround, as 'preventDefault' and everything above seems to be not working
event . originalEvent [ "dismissed" ] = true
}
}
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-04-06 03:06:50 +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 ) ,
] )
2022-03-29 00:20:10 +02:00
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-06-24 16:47:00 +02:00
public static MapToObj < V , T > (
d : Map < string , V > ,
onValue : ( t : V , key : string ) = > T
) : Record < string , T > {
2022-03-18 01:21:00 +01:00
const o = { }
2022-04-22 16:51:49 +02:00
const keys = Array . from ( d . keys ( ) )
keys . sort ( )
for ( const key of keys ) {
2022-06-24 16:47:00 +02:00
o [ key ] = onValue ( d . get ( key ) , key )
2022-04-22 16:51:49 +02:00
}
2022-03-18 01:21:00 +01:00
return o
}
2022-06-24 16:47:00 +02:00
/ * *
* Switches keys and values around
*
* Utils . TransposeMap ( { "a" : [ "b" , "c" ] , "x" : [ "b" , "y" ] } ) // => {"b" : ["a", "x"], "c" : ["a"], "y" : ["x"]}
* /
public static TransposeMap < K extends string , V extends string > (
d : Record < K , V [ ] >
) : Record < V , K [ ] > {
const newD : Record < V , K [ ] > = < any > { }
for ( const k in d ) {
const vs = d [ k ]
for ( let v of vs ) {
const list = newD [ v ]
if ( list === undefined ) {
newD [ v ] = [ k ] // Left: indexing; right: list with one element
} else {
list . push ( k )
}
}
}
return newD
}
2022-03-18 01:21:00 +01:00
/ * *
* Utils . colorAsHex ( { r : 255 , g : 128 , b : 0 } ) // => "#ff8000"
* Utils . colorAsHex ( undefined ) // => undefined
* /
2022-04-06 03:06:50 +02:00
public static colorAsHex ( c : { r : number ; g : number ; b : number } ) {
if ( c === undefined ) {
2022-03-18 01:21:00 +01:00
return undefined
}
2022-04-06 03:06:50 +02:00
2022-03-18 01:21:00 +01:00
function componentToHex ( n ) {
let hex = n . toString ( 16 )
return hex . length == 1 ? "0" + hex : hex
2021-10-18 22:17:15 +02:00
}
2022-04-06 03:06:50 +02:00
2022-03-18 01:21:00 +01:00
return "#" + componentToHex ( c . r ) + componentToHex ( c . g ) + componentToHex ( c . b )
}
2022-04-06 03:06:50 +02:00
2022-03-18 01:21:00 +01:00
/ * *
2022-04-06 03:06:50 +02:00
*
2022-03-18 01:21:00 +01:00
* 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 } {
2022-04-06 03:06:50 +02:00
if ( hex === undefined ) {
2022-03-18 01:21:00 +01:00
return undefined
}
hex = hex . replace ( /[ \t]/g , "" )
if ( hex . startsWith ( "rgba(" ) ) {
2022-04-06 03:06:50 +02:00
const match = hex . match ( /rgba\(([0-9.]+),([0-9.]+),([0-9.]+)(,[0-9.]*)?\)/ )
if ( match == undefined ) {
2022-03-18 01:21:00 +01:00
return undefined
}
2022-04-06 03:06:50 +02:00
return { r : Number ( match [ 1 ] ) , g : Number ( match [ 2 ] ) , b : Number ( match [ 3 ] ) }
2022-03-18 01:21:00 +01:00
}
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-04-06 03:06:50 +02: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
}
2022-04-06 03:06:50 +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-07-25 18:55:15 +02:00
static toIdRecord < T extends { id : string } > ( ts : T [ ] ) : Record < string , T > {
const result : Record < string , T > = { }
for ( const t of ts ) {
result [ t . id ] = t
}
return result
}
2022-09-08 21:40:48 +02:00
2022-08-18 23:37:44 +02:00
public static SetMidnight ( d : Date ) : void {
d . setUTCHours ( 0 )
d . setUTCSeconds ( 0 )
d . setUTCMilliseconds ( 0 )
d . setUTCMinutes ( 0 )
}
2022-12-06 03:43:54 +01:00
public static findParentWithScrolling ( element : HTMLElement ) : HTMLElement {
// Check if the element itself has scrolling
if ( element . scrollHeight > element . clientHeight ) {
2022-12-16 13:45:07 +01:00
return element
2022-12-06 03:43:54 +01:00
}
// If the element does not have scrolling, check if it has a parent element
if ( ! element . parentElement ) {
2022-12-16 13:45:07 +01:00
return null
2022-12-06 03:43:54 +01:00
}
// If the element has a parent, repeat the process for the parent element
2022-12-16 13:45:07 +01:00
return Utils . findParentWithScrolling ( element . parentElement )
2022-12-06 03:43:54 +01:00
}
2021-05-06 03:03:54 +02:00
}