2023-09-29 11:10:24 +02:00
import colors from "./assets/colors.json" ;
import DOMPurify from "dompurify" ;
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
* /
2023-09-29 11:10:24 +02:00
public static runningFromConsole = typeof window === "undefined" ;
public static readonly assets_path = "./assets/svg/" ;
2022-05-26 13:23:25 +02:00
public static externalDownloadFunction : (
url : string ,
headers? : any
2023-09-29 11:10:24 +02:00
) = > 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 )
2023-09-29 11:10:24 +02: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 .
2023-09-29 11:10:24 +02:00
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" ,
2023-09-29 11:10:24 +02:00
"width"
] ;
2022-01-07 17:31:39 +01:00
private static extraKeys = [
"nl" ,
"en" ,
"fr" ,
"de" ,
"pt" ,
"es" ,
"name" ,
"phone" ,
"email" ,
"amenity" ,
"leisure" ,
"highway" ,
"building" ,
"yes" ,
"no" ,
"true" ,
2023-09-29 11:10:24 +02:00
"false"
] ;
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
}
2023-09-29 11:10:24 +02:00
> ( ) ;
2021-12-09 13:16:40 +01:00
2023-09-21 01:53:34 +02:00
public static initDomPurify() {
if ( Utils . runningFromConsole ) {
2023-09-29 11:10:24 +02:00
return ;
2023-09-21 01:53:34 +02:00
}
2023-09-29 11:10:24 +02:00
DOMPurify . addHook ( "afterSanitizeAttributes" , function ( node ) {
2023-09-21 01:53:34 +02:00
// set all elements owning target to target=_blank + add noopener noreferrer
2023-09-29 11:10:24 +02:00
const target = node . getAttribute ( "target" ) ;
2023-09-22 11:20:22 +02:00
if ( target ) {
2023-09-29 11:10:24 +02:00
node . setAttribute ( "target" , "_blank" ) ;
node . setAttribute ( "rel" , "noopener noreferrer" ) ;
2023-09-21 01:53:34 +02:00
}
2023-09-29 11:10:24 +02:00
} ) ;
2023-09-21 01:53:34 +02:00
}
public static purify ( src : string ) : string {
return DOMPurify . sanitize ( src , {
USE_PROFILES : { html : true } ,
2023-09-29 11:10:24 +02:00
ADD_ATTR : [ "target" ] // Don't remove target='_blank'. Note that Utils.initDomPurify does add a hook which automatically adds 'rel=noopener'
} ) ;
2023-09-21 01:53:34 +02:00
}
2021-12-09 13:16:40 +01:00
/ * *
* Parses the arguments for special visualisations
* /
public static ParseVisArgs (
specs : { name : string ; defaultValue? : string } [ ] ,
args : string [ ]
2023-05-30 02:52:22 +02:00
) : Record < string , string > {
2023-09-29 11:10:24 +02:00
const parsed : Record < string , string > = { } ;
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"
2023-09-29 11:10:24 +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 ++ ) {
2023-09-29 11:10:24 +02:00
const spec = specs [ i ] ;
let arg = args [ i ] ? . trim ( ) ;
2022-01-07 17:31:39 +01:00
if ( arg === undefined || arg === "" ) {
2023-09-29 11:10:24 +02:00
arg = spec . defaultValue ;
2021-12-09 13:16:40 +01:00
}
2023-09-29 11:10:24 +02:00
parsed [ spec . name ] = arg ;
2021-12-09 13:16:40 +01:00
}
2023-09-29 11:10:24 +02:00
return parsed ;
2021-12-09 13:16:40 +01:00
}
2020-09-30 22:22:58 +02:00
static EncodeXmlValue ( str ) {
2021-05-18 19:48:20 +02:00
if ( typeof str !== "string" ) {
2023-09-29 11:10:24 +02:00
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 , """ )
2023-09-29 11:10:24 +02:00
. replace ( /'/g , "'" ) ;
2020-09-30 22:22:58 +02:00
}
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 ) {
2023-09-29 11:10:24 +02:00
const i = parseFloat ( str ) ;
2020-07-24 01:12:57 +02:00
if ( isNaN ( i ) ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2020-07-24 01:12:57 +02:00
}
2023-09-29 11:10:24 +02:00
return i ;
2020-07-24 01:12:57 +02:00
}
2023-09-29 11:10:24 +02:00
return undefined ;
2020-07-24 01:12:57 +02:00
}
2020-09-30 22:22:58 +02:00
public static Upper ( str : string ) {
2023-09-29 11:10:24 +02:00
return str . substr ( 0 , 1 ) . toUpperCase ( ) + str . substr ( 1 ) ;
2020-09-30 22:22:58 +02:00
}
2020-10-04 01:04:46 +02:00
public static TwoDigits ( i : number ) {
if ( i < 10 ) {
2023-09-29 11:10:24 +02:00
return "0" + i ;
2020-10-04 01:04:46 +02:00
}
2023-09-29 11:10:24 +02:00
return "" + i ;
2020-10-04 01:04:46 +02:00
}
2022-03-15 13:40:23 +01:00
/ * *
2023-06-01 02:52:21 +02:00
* Converts a number to a number with precisely 7 decimals
2022-03-18 01:21:00 +01:00
*
2023-06-01 02:52:21 +02:00
* Utils . Round7 ( 12.123456789 ) // => 12.1234568
2022-03-15 13:40:23 +01:00
* /
2023-06-01 02:52:21 +02:00
public static Round7 ( i : number ) : number {
2023-08-10 15:37:44 +02:00
if ( i == undefined ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2023-08-10 15:37:44 +02:00
}
2023-09-29 11:10:24 +02:00
return Math . round ( i * 10000000 ) / 10000000 ;
2020-10-30 00:56:46 +01:00
}
2020-10-04 01:04:46 +02:00
public static Times ( f : ( i : number ) = > string , count : number ) : string {
2023-09-29 11:10:24 +02:00
let res = "" ;
2020-09-30 22:22:58 +02:00
for ( let i = 0 ; i < count ; i ++ ) {
2023-09-29 11:10:24 +02:00
res += f ( i ) ;
2020-09-30 22:22:58 +02:00
}
2023-09-29 11:10:24 +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 [ ] {
2023-09-29 11:10:24 +02:00
const res : T [ ] = [ ] ;
2021-06-16 14:23:53 +02:00
for ( let i = 0 ; i < count ; i ++ ) {
2023-09-29 11:10:24 +02:00
res . push ( f ( i ) ) ;
2021-06-16 14:23:53 +02:00
}
2023-09-29 11:10:24 +02:00
return res ;
2021-06-16 14:23:53 +02:00
}
2022-08-17 02:44:09 +02:00
public static NoNull < T > ( array : T [ ] ) : NonNullable < T > [ ] {
2023-09-29 11:10:24 +02:00
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 > {
2023-09-29 11:10:24 +02:00
const hist = new Map < string , number > ( ) ;
2021-10-04 03:12:42 +02:00
for ( const s of array ) {
2023-09-29 11:10:24 +02:00
hist . set ( s , 1 + ( hist . get ( s ) ? ? 0 ) ) ;
2021-10-04 03:12:42 +02:00
}
2023-09-29 11:10:24 +02:00
return hist ;
2021-10-04 03:12:42 +02:00
}
2021-10-18 22:17:15 +02:00
2023-02-03 03:55:33 +01:00
/ * *
* Removes all empty strings from this list
* If undefined or null is given , an empty list is returned
*
* Utils . NoEmpty ( undefined ) // => []
* Utils . NoEmpty ( [ "abc" , "" , "def" , null ] ) // => ["abc","def", null]
*
* /
2021-03-09 13:10:48 +01:00
public static NoEmpty ( array : string [ ] ) : string [ ] {
2023-09-29 11:10:24 +02:00
const ls : string [ ] = [ ] ;
2023-02-10 14:28:00 +01:00
if ( ! array ) {
2023-09-29 11:10:24 +02:00
return ls ;
2023-02-03 03:55:33 +01:00
}
2020-08-22 13:02:31 +02:00
for ( const t of array ) {
if ( t === "" ) {
2023-09-29 11:10:24 +02:00
continue ;
2020-08-22 13:02:31 +02:00
}
2023-09-29 11:10:24 +02:00
ls . push ( t ) ;
2020-08-22 13:02:31 +02:00
}
2023-09-29 11:10:24 +02:00
return ls ;
2020-08-22 13:02:31 +02:00
}
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 ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2020-09-03 03:16:43 +02:00
}
2021-03-09 13:10:48 +01:00
if ( str . length <= l ) {
2023-09-29 11:10:24 +02:00
return str ;
2020-08-26 00:21:34 +02:00
}
2023-09-29 11:10:24 +02:00
return str . substr ( 0 , l - 3 ) + "..." ;
2020-08-26 00:21:34 +02:00
}
2022-04-06 03:06:50 +02:00
2023-03-21 20:59:31 +01:00
/ * *
2023-09-22 11:20:22 +02:00
* Adds a property to the given object , but the value will _only_ be calculated when it is actually requested .
* This calculation will run once
2023-03-21 20:59:31 +01:00
* @param object
* @param name
* @param init
2023-09-22 11:20:22 +02:00
* @param whenDone : called when the value is updated . Note that this will be called at most once
2023-03-21 20:59:31 +01:00
* @constructor
* /
2023-04-15 02:28:24 +02:00
public static AddLazyProperty (
object : any ,
name : string ,
init : ( ) = > any ,
whenDone ? : ( ) = > void
) {
2023-03-21 20:59:31 +01:00
Object . defineProperty ( object , name , {
enumerable : false ,
configurable : true ,
get : ( ) = > {
2023-09-29 11:10:24 +02:00
delete object [ name ] ;
object [ name ] = init ( ) ;
2023-04-15 02:28:24 +02:00
if ( whenDone ) {
2023-09-29 11:10:24 +02:00
whenDone ( ) ;
2023-04-15 02:28:24 +02:00
}
2023-09-29 11:10:24 +02:00
return object [ name ] ;
}
} ) ;
2023-03-21 20:59:31 +01:00
}
/ * *
* Adds a property to the given object , but the value will _only_ be calculated when it is actually requested
* /
public static AddLazyPropertyAsync (
object : any ,
name : string ,
init : ( ) = > Promise < any > ,
whenDone ? : ( ) = > void
) {
Object . defineProperty ( object , name , {
enumerable : false ,
configurable : true ,
get : ( ) = > {
init ( ) . then ( ( r ) = > {
2023-09-29 11:10:24 +02:00
delete object [ name ] ;
object [ name ] = r ;
2023-03-21 20:59:31 +01:00
if ( whenDone ) {
2023-09-29 11:10:24 +02:00
whenDone ( ) ;
2023-03-21 20:59:31 +01:00
}
2023-09-29 11:10:24 +02:00
} ) ;
}
} ) ;
2023-03-21 20:59:31 +01:00
}
2022-04-01 21:17:27 +02:00
public static FixedLength ( str : string , l : number ) {
2023-09-29 11:10:24 +02:00
str = Utils . EllipsesAfter ( str , l ) ;
2022-04-06 03:06:50 +02:00
while ( str . length < l ) {
2023-09-29 11:10:24 +02:00
str = " " + str ;
2022-04-01 21:17:27 +02:00
}
2023-09-29 11:10:24 +02:00
return str ;
2022-04-01 21:17:27 +02:00
}
2021-03-09 13:10:48 +01:00
2023-04-07 04:23:45 +02:00
/ * *
* Creates a new array with all elements from 'arr' in such a way that every element will be kept only once
* Elements are returned in the same order as they appear in the lists
* @param arr
* @constructor
* /
2021-03-09 13:10:48 +01:00
public static Dedup ( arr : string [ ] ) : string [ ] {
if ( arr === undefined ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2020-08-26 15:36:04 +02:00
}
2023-09-29 11:10:24 +02:00
const newArr = [ ] ;
2020-08-26 15:36:04 +02:00
for ( const string of arr ) {
2020-10-02 19:00:24 +02:00
if ( newArr . indexOf ( string ) < 0 ) {
2023-09-29 11:10:24 +02:00
newArr . push ( string ) ;
2020-08-26 15:36:04 +02:00
}
}
2023-09-29 11:10:24 +02:00
return newArr ;
2020-08-26 15:36:04 +02:00
}
2021-09-09 00:05:51 +02:00
2023-09-24 00:25:10 +02:00
public static Duplicates ( arr : string [ ] ) : string [ ] {
2021-11-08 03:00:58 +01:00
if ( arr === undefined ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2021-11-08 03:00:58 +01:00
}
2023-09-29 11:10:24 +02:00
const newArr = [ ] ;
const seen = new Set < string > ( ) ;
2021-11-08 03:00:58 +01:00
for ( const string of arr ) {
2022-01-07 17:31:39 +01:00
if ( seen . has ( string ) ) {
2023-09-29 11:10:24 +02:00
newArr . push ( string ) ;
2021-11-08 03:00:58 +01:00
}
2023-09-29 11:10:24 +02:00
seen . add ( string ) ;
2021-11-08 03:00:58 +01:00
}
2023-09-29 11:10:24 +02:00
return newArr ;
2021-11-08 03:00:58 +01:00
}
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 [ ] {
2023-09-29 11:10:24 +02:00
const result = [ ] ;
2022-04-06 03:06:50 +02:00
for ( const value of list ) {
if ( Array . isArray ( value ) ) {
2023-09-29 11:10:24 +02:00
result . push ( . . . value ) ;
2022-04-06 03:06:50 +02:00
} else {
2023-09-29 11:10:24 +02:00
result . push ( value ) ;
2022-04-06 03:06:50 +02:00
}
}
2023-09-29 11:10:24 +02:00
return result ;
2022-04-06 03:06:50 +02:00
}
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 ) {
2023-09-29 11:10:24 +02:00
return false ;
2021-07-15 20:47:28 +02:00
}
2023-09-29 11:10:24 +02:00
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 ] ) ) {
2023-09-29 11:10:24 +02:00
return false ;
2021-07-15 20:47:28 +02:00
}
}
2023-09-29 11:10:24 +02:00
return true ;
2021-07-15 20:47:28 +02:00
}
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 ) {
2023-09-29 11:10:24 +02:00
const t = { } ;
2020-08-27 00:08:00 +02:00
for ( const k in a ) {
2023-09-29 11:10:24 +02:00
t [ k ] = a [ k ] ;
2020-08-27 00:08:00 +02:00
}
for ( const k in b ) {
2023-09-29 11:10:24 +02:00
t [ k ] = b [ k ] ;
2020-08-27 00:08:00 +02:00
}
2023-09-29 11:10:24 +02:00
return t ;
2020-08-27 00:08:00 +02:00
}
2020-10-02 19:00:24 +02:00
public static SplitFirst ( a : string , sep : string ) : string [ ] {
2023-09-29 11:10:24 +02:00
const index = a . indexOf ( sep ) ;
2020-10-02 19:00:24 +02:00
if ( index < 0 ) {
2023-09-29 11:10:24 +02:00
return [ a ] ;
2020-08-31 02:59:47 +02:00
}
2023-09-29 11:10: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
2023-07-17 01:07:01 +02:00
*
* Utils . SubstituteKeys ( "abc{def}ghi" , { def : 'XYZ' } ) // => "abcXYZghi"
* Utils . SubstituteKeys ( "abc{def}{def}ghi" , { def : 'XYZ' } ) // => "abcXYZXYZghi"
* Utils . SubstituteKeys ( "abc{def}ghi" , { def : '{XYZ}' } ) // => "abc{XYZ}ghi"
2023-09-15 01:53:50 +02:00
* Utils . SubstituteKeys ( "abc\n\n{def}ghi" , { def : '{XYZ}' } ) // => "abc\n\n{XYZ}ghi"
2023-07-17 01:07:01 +02:00
*
2022-01-07 17:31:39 +01:00
* @param txt
* @param tags
* @param useLang
* @constructor
* /
2022-09-02 12:35:23 +02:00
public static SubstituteKeys (
txt : string | undefined ,
2023-09-21 02:31:35 +02:00
tags : Record < string , any > | undefined ,
2022-09-02 12:35:23 +02:00
useLang? : string
) : string | undefined {
2021-10-22 01:07:32 +02:00
if ( txt === undefined ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2021-10-22 01:07:32 +02:00
}
2023-09-29 11:10:24 +02:00
const regex = / ( . * ? ) { ( [ ^ } ] * ) } ( . * ) / s ;
2021-10-18 22:17:15 +02:00
2023-09-29 11:10:24 +02:00
let match = txt . match ( regex ) ;
2021-10-18 22:17:15 +02:00
2023-07-27 12:47:19 +02:00
if ( ! match ) {
2023-09-29 11:10:24 +02:00
return txt ;
2023-07-17 01:07:01 +02:00
}
2023-09-29 11:10:24 +02:00
let result = "" ;
2021-10-18 22:17:15 +02:00
while ( match ) {
2023-09-29 11:10:24 +02:00
const [ _ , normal , key , leftover ] = match ;
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
2023-09-29 11:10:24 +02:00
const date : Date = el ;
v = date . toISOString ( ) ;
2021-12-06 03:24:33 +01:00
}
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 [ "*" ] ? ?
2023-09-29 11:10:24 +02:00
( v . textFor !== undefined ? v . textFor ( useLang ) : v ) ;
2021-12-14 17:29:21 +01:00
}
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
2023-09-29 11:10:24 +02:00
) ;
v = v . InnerConstructElement ( ) ? . textContent ;
2021-12-06 03:24:33 +01:00
}
2022-01-07 17:31:39 +01:00
if ( typeof v !== "string" ) {
2023-09-29 11:10:24 +02:00
v = "" + v ;
2021-11-07 18:37:42 +01:00
}
2023-09-29 11:10:24 +02: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
2023-09-29 11:10:24 +02:00
v = "" ;
2021-11-07 18:37:42 +01:00
}
2021-10-22 01:07:32 +02:00
2023-09-29 11:10:24 +02:00
result += normal + v ;
match = leftover . match ( regex ) ;
2023-07-27 12:47:19 +02:00
if ( ! match ) {
2023-09-29 11:10:24 +02:00
result += leftover ;
2023-07-17 01:07:01 +02:00
}
}
2023-09-29 11:10:24 +02:00
return result ;
2021-06-14 02:39:23 +02:00
}
2021-03-09 13:10:48 +01:00
public static LoadCustomCss ( location : string ) {
2023-09-29 11:10:24 +02:00
const head = document . getElementsByTagName ( "head" ) [ 0 ] ;
const link = document . createElement ( "link" ) ;
link . id = "customCss" ;
link . rel = "stylesheet" ;
link . type = "text/css" ;
link . href = location ;
link . media = "all" ;
head . appendChild ( link ) ;
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 ) {
2023-09-29 11:10:24 +02:00
return ;
2023-01-17 02:54:33 +01:00
}
2023-09-29 11:10:24 +02:00
target . push ( . . . source ) ;
}
/ * *
* Recursively rewrites all keys from ` +key ` , ` key+ ` and ` =key ` into ` key
*
* Utils . CleanMergeObject ( { "condition" : { "and+" : [ "xyz" ] } } // => {"condition":{"and":["xyz"]}}
* @param obj
* @constructor
* @private
* /
private static CleanMergeObject ( obj : any ) {
if ( Array . isArray ( obj ) ) {
const result = [ ]
for ( const el of obj ) {
result . push ( Utils . CleanMergeObject ( el ) )
}
return result
}
if ( typeof obj !== "object" ) {
return obj ;
}
const newObj = { }
for ( let objKey in obj ) {
let cleanKey = objKey
if ( objKey . startsWith ( "+" ) || objKey . startsWith ( "=" ) ) {
cleanKey = objKey . substring ( 1 )
} else if ( objKey . endsWith ( "+" ) || objKey . endsWith ( "=" ) ) {
cleanKey = objKey . substring ( 0 , objKey . length - 1 )
}
newObj [ cleanKey ] = Utils . CleanMergeObject ( obj [ objKey ] )
}
return newObj
2023-01-17 02:54:33 +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
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"
2023-09-29 11:10:24 +02:00
*
* const source = { "condition" : { "+and" : [ "xyz" ] } }
* const target = { "id" : "test" }
* const result = Utils . Merge ( source , target )
* result // => {"id":"test","condition":{"and":["xyz"]}}
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 ) {
2023-09-29 11:10:24 +02:00
return < T & S > Utils . CleanMergeObject ( 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 ) ) {
2023-09-29 11:10:24 +02:00
continue ;
2021-05-19 22:38:05 +02:00
}
2022-02-01 04:14:54 +01:00
if ( key . startsWith ( "=" ) ) {
2023-09-29 11:10:24 +02:00
const trimmedKey = key . substr ( 1 ) ;
target [ trimmedKey ] = source [ key ] ;
continue ;
2022-02-01 04:14:54 +01:00
}
2021-06-20 03:06:00 +02:00
if ( key . startsWith ( "+" ) || key . endsWith ( "+" ) ) {
2023-09-29 11:10:24 +02:00
const trimmedKey = key . replace ( "+" , "" ) ;
const sourceV = source [ key ] ;
const targetV = target [ trimmedKey ] ? ? [ ] ;
2021-06-20 03:06:00 +02:00
2023-09-29 11:10:24 +02:00
let newList : any [ ] ;
2021-06-20 03:06:00 +02:00
if ( key . startsWith ( "+" ) ) {
2023-09-29 11:10:24 +02:00
if ( ! Array . isArray ( targetV ) ) {
throw new Error ( "Cannot concatenate: value to add is not an array: " + JSON . stringify ( targetV ) )
}
if ( Array . isArray ( sourceV ) ) {
newList = sourceV . concat ( targetV ) ? ? targetV ;
} else {
throw new Error ( "Could not merge concatenate " + JSON . stringify ( sourceV ) + " and " + JSON . stringify ( targetV ) ) ;
}
2021-06-20 03:06:00 +02:00
} else {
2023-09-29 11:10:24 +02:00
newList = targetV . concat ( sourceV ? ? [ ] ) ;
2021-06-20 03:06:00 +02:00
}
2023-09-29 11:10:24 +02:00
target [ trimmedKey ] = newList ;
continue ;
2021-06-20 03:06:00 +02:00
}
2023-09-29 11:10:24 +02:00
const sourceV = source [ key ] ;
2022-03-15 01:42:38 +01:00
// @ts-ignore
2023-09-29 11:10:24 +02: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
2023-09-29 11:10:24 +02:00
target [ key ] = null ;
2021-07-27 19:35:43 +02:00
} else if ( targetV === undefined ) {
2022-03-15 01:42:38 +01:00
// @ts-ignore
2023-09-29 11:10:24 +02:00
target [ key ] = Utils . CleanMergeObject ( sourceV ) ;
2021-03-09 13:10:48 +01:00
} else {
2023-09-29 11:10:24 +02:00
Utils . Merge ( sourceV , targetV ) ;
2021-01-06 02:52:38 +01:00
}
2021-03-09 13:10:48 +01:00
} else {
2022-03-15 01:42:38 +01:00
// @ts-ignore
2023-09-29 11:10:24 +02:00
target [ key ] = Utils . CleanMergeObject ( sourceV ) ;
2021-01-06 02:52:38 +01:00
}
}
2022-03-15 01:42:38 +01:00
// @ts-ignore
2023-09-29 11:10:24 +02:00
return target ;
2021-01-06 02:52:38 +01:00
}
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 ) {
2023-09-29 11:10:24 +02:00
return ;
2022-06-21 16:47:54 +02:00
}
2022-09-08 21:40:48 +02:00
2023-09-29 11:10:24 +02:00
const head = path [ 0 ] ;
2022-02-09 22:34:02 +01:00
if ( path . length === 1 ) {
// We have reached the leaf
2023-09-29 11:10:24 +02:00
const leaf = object [ head ] ;
2022-02-09 22:34:02 +01:00
if ( leaf !== undefined ) {
2022-02-19 02:45:15 +01:00
if ( Array . isArray ( leaf ) ) {
2023-09-29 11:10:24 +02:00
object [ head ] = leaf . map ( ( o ) = > replaceLeaf ( o , travelledPath ) ) ;
2022-02-19 02:45:15 +01:00
} else {
2023-09-29 11:10:24 +02:00
object [ head ] = replaceLeaf ( leaf , travelledPath ) ;
2022-12-16 13:45:07 +01:00
if ( object [ head ] === undefined ) {
2023-09-29 11:10:24 +02:00
delete object [ head ] ;
2022-12-13 11:32:47 +01:00
}
2022-02-09 22:34:02 +01:00
}
}
2023-09-29 11:10:24 +02:00
return ;
2022-02-09 22:34:02 +01:00
}
2023-09-29 11:10:24 +02:00
const sub = object [ head ] ;
2022-02-09 22:34:02 +01:00
if ( sub === undefined ) {
2023-09-29 11:10:24 +02:00
return ;
2022-02-09 22:34:02 +01:00
}
if ( typeof sub !== "object" ) {
2023-09-29 11:10:24 +02:00
return ;
2022-02-09 22:34:02 +01:00
}
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 ] )
2023-09-29 11:10:24 +02:00
) ;
return ;
2022-02-09 22:34:02 +01:00
}
2023-09-29 11:10:24 +02: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 ) {
2023-09-29 11:10:24 +02:00
return collectedList ;
2022-02-09 22:34:02 +01:00
}
2023-09-29 11:10:24 +02:00
const head = path [ 0 ] ;
travelledPath = [ . . . travelledPath , head ] ;
2022-02-09 22:34:02 +01:00
if ( path . length === 1 ) {
// We have reached the leaf
2023-09-29 11:10:24 +02:00
const leaf = object [ head ] ;
2022-02-09 22:34:02 +01:00
if ( leaf === undefined || leaf === null ) {
2023-09-29 11:10:24 +02:00
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 ++ ) {
2023-09-29 11:10:24 +02:00
const l = ( < any [ ] > leaf ) [ i ] ;
collectedList . push ( { leaf : l , path : [ . . . travelledPath , "" + i ] } ) ;
2022-02-09 22:34:02 +01:00
}
2022-02-19 02:45:15 +01:00
} else {
2023-09-29 11:10:24 +02:00
collectedList . push ( { leaf , path : travelledPath } ) ;
2022-02-19 02:45:15 +01:00
}
2023-09-29 11:10:24 +02:00
return collectedList ;
2022-02-09 22:34:02 +01:00
}
2023-09-29 11:10:24 +02:00
const sub = object [ head ] ;
2022-02-09 22:34:02 +01:00
if ( sub === undefined || sub === null ) {
2023-09-29 11:10:24 +02:00
return collectedList ;
2022-02-09 22:34:02 +01:00
}
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 ] )
2023-09-29 11:10:24 +02:00
) ;
return collectedList ;
2022-02-09 22:34:02 +01:00
}
if ( typeof sub !== "object" ) {
2023-09-29 11:10:24 +02:00
return collectedList ;
2022-02-09 22:34:02 +01:00
}
2023-09-29 11:10:24 +02: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 ) {
2023-09-29 11:10:24 +02:00
return f ( json , path ) ;
2022-01-07 17:31:39 +01:00
}
2023-09-29 11:10:24 +02:00
const jtp = typeof json ;
2022-03-18 01:21:00 +01:00
if ( isLeaf !== undefined ) {
if ( jtp === "object" ) {
if ( isLeaf ( json ) ) {
2023-09-29 11:10:24 +02:00
return f ( json , path ) ;
2022-03-08 04:09:03 +01:00
}
} else {
2023-09-29 11:10:24 +02:00
return json ;
2022-03-08 04:09:03 +01:00
}
2022-03-18 01:21:00 +01:00
} else if ( jtp === "boolean" || jtp === "string" || jtp === "number" ) {
2023-09-29 11:10:24 +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 ) = > {
2023-09-29 11:10:24 +02:00
return Utils . WalkJson ( sub , f , isLeaf , [ . . . path , "" + i ] ) ;
} ) ;
2022-01-07 17:31:39 +01:00
}
2023-09-29 11:10:24 +02:00
const cp = { . . . json } ;
2022-01-07 17:31:39 +01:00
for ( const key in json ) {
2023-09-29 11:10:24 +02:00
cp [ key ] = Utils . WalkJson ( json [ key ] , f , isLeaf , [ . . . path , key ] ) ;
2022-01-07 17:31:39 +01:00
}
2023-09-29 11:10:24 +02:00
return cp ;
2022-01-07 17:31:39 +01:00
}
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 ) {
2023-09-29 11:10:24 +02:00
return ;
2022-04-01 12:51:55 +02:00
}
2023-09-29 11:10:24 +02:00
const jtp = typeof json ;
2022-04-01 12:51:55 +02:00
if ( isLeaf !== undefined ) {
if ( jtp !== "object" ) {
2023-09-29 11:10:24 +02:00
return ;
2022-04-01 12:51:55 +02:00
}
2022-04-06 03:06:50 +02:00
2022-04-01 12:51:55 +02:00
if ( isLeaf ( json ) ) {
2023-09-29 11:10:24 +02:00
return collect ( json , path ) ;
2022-04-01 12:51:55 +02:00
}
} else if ( jtp === "boolean" || jtp === "string" || jtp === "number" ) {
2023-09-29 11:10:24 +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 ) = > {
2023-09-29 11:10:24 +02:00
return Utils . WalkObject ( sub , collect , isLeaf , [ . . . path , i ] ) ;
} ) ;
return ;
2022-04-01 12:51:55 +02:00
}
for ( const key in json ) {
2023-09-29 11:10:24 +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 ) {
2023-09-29 11:10:24 +02:00
const found = dict . get ( k ) ;
2021-03-09 13:10:48 +01:00
if ( found !== undefined ) {
2023-09-29 11:10:24 +02:00
return found ;
2021-03-09 13:10:48 +01:00
}
2023-09-29 11:10:24 +02:00
dict . set ( k , v ( ) ) ;
return dict . get ( k ) ;
2021-03-09 13:10:48 +01:00
}
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 {
2023-09-29 11:10:24 +02:00
stringified = stringified . replace ( /\|/g , "||" ) ;
2021-04-04 03:22:56 +02:00
2023-09-29 11:10:24 +02:00
const keys = Utils . knownKeys . concat ( Utils . extraKeys ) ;
2021-04-04 03:22:56 +02:00
for ( let i = 0 ; i < keys . length ; i ++ ) {
2023-09-29 11:10:24 +02:00
const knownKey = keys [ i ] ;
let code = i ;
2021-04-04 03:22:56 +02:00
if ( i >= 124 ) {
2023-09-29 11:10:24 +02:00
code += 1 ; // Character 127 is our 'escape' character |
2021-04-04 03:22:56 +02:00
}
2023-09-29 11:10:24 +02:00
const replacement = "|" + String . fromCharCode ( code ) ;
stringified = stringified . replace ( new RegExp ( ` \ " ${ knownKey } \ ": ` , "g" ) , replacement ) ;
2021-04-04 03:22:56 +02:00
}
2023-09-29 11:10:24 +02:00
return stringified ;
2021-04-04 03:22:56 +02:00
}
public static UnMinify ( minified : string ) : string {
2021-07-03 22:24:12 +02:00
if ( minified === undefined || minified === null ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2021-06-14 02:39:23 +02:00
}
2021-07-03 22:24:12 +02:00
2023-09-29 11:10:24 +02:00
const parts = minified . split ( "|" ) ;
let result = parts . shift ( ) ;
const keys = Utils . knownKeys . concat ( Utils . extraKeys ) ;
2021-04-04 03:22:56 +02:00
for ( const part of parts ) {
if ( part == "" ) {
// Empty string => this was a || originally
2023-09-29 11:10:24 +02:00
result += "|" ;
continue ;
2021-04-04 03:22:56 +02:00
}
2023-09-29 11:10:24 +02:00
const i = part . charCodeAt ( 0 ) ;
result += "\"" + keys [ i ] + "\":" + part . substring ( 1 ) ;
2021-04-04 03:22:56 +02:00
}
2023-09-29 11:10:24 +02:00
return result ;
2021-04-04 03:22:56 +02:00
}
2021-09-22 16:07:56 +02:00
public static injectJsonDownloadForTests ( url : string , data ) {
2023-09-29 11:10:24 +02:00
Utils . injectedDownloads [ url ] = data ;
2021-09-22 16:07:56 +02:00
}
2022-05-26 13:23:25 +02:00
public static async download ( url : string , headers? : any ) : Promise < string | undefined > {
2023-09-29 11:10:24 +02:00
const result = await Utils . downloadAdvanced ( url , headers ) ;
2022-12-16 13:45:07 +01:00
if ( result [ "error" ] !== undefined ) {
2023-09-29 11:10:24 +02:00
throw result [ "error" ] ;
2022-12-16 01:00:43 +01:00
}
2023-09-29 11:10:24 +02:00
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 ) {
2023-09-29 11:10:24 +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 ) = > {
2023-09-29 11:10:24 +02:00
const xhr = new XMLHttpRequest ( ) ;
2021-09-22 16:07:56 +02:00
xhr . onload = ( ) = > {
if ( xhr . status == 200 ) {
2023-09-29 11:10:24 +02:00
resolve ( { content : xhr.response } ) ;
2022-05-26 13:23:25 +02:00
} else if ( xhr . status === 302 ) {
2023-09-29 11:10:24 +02:00
resolve ( { redirect : xhr.getResponseHeader ( "location" ) } ) ;
2021-09-28 18:40:45 +02:00
} else if ( xhr . status === 509 || xhr . status === 429 ) {
2023-09-29 11:10:24 +02: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 ,
2023-09-29 11:10:24 +02:00
statuscode : xhr.status
} ) ;
2021-09-26 17:36:39 +02:00
}
2023-09-29 11:10:24 +02:00
} ;
xhr . open ( "GET" , url ) ;
2021-11-12 04:11:53 +01:00
if ( headers !== undefined ) {
for ( const key in headers ) {
2023-09-29 11:10:24 +02:00
xhr . setRequestHeader ( key , headers [ key ] ) ;
2022-09-08 21:40:48 +02:00
}
}
2023-09-29 11:10:24 +02:00
xhr . send ( ) ;
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 ) = > {
2023-09-29 11:10:24 +02:00
const xhr = new XMLHttpRequest ( ) ;
2021-11-12 04:11:53 +01:00
xhr . onload = ( ) = > {
if ( xhr . status == 200 ) {
2023-09-29 11:10:24 +02:00
resolve ( xhr . response ) ;
2021-11-12 04:11:53 +01:00
} else if ( xhr . status === 509 || xhr . status === 429 ) {
2023-09-29 11:10:24 +02:00
reject ( "rate limited" ) ;
2021-11-12 04:11:53 +01:00
} else {
2023-09-29 11:10:24 +02:00
reject ( xhr . statusText ) ;
2021-11-12 04:11:53 +01:00
}
2023-09-29 11:10:24 +02:00
} ;
xhr . open ( "POST" , url ) ;
2021-11-12 04:11:53 +01:00
if ( headers !== undefined ) {
for ( const key in headers ) {
2023-09-29 11:10:24 +02:00
xhr . setRequestHeader ( key , headers [ key ] ) ;
2021-11-12 04:11:53 +01:00
}
}
2023-09-29 11:10:24 +02:00
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 > {
2023-09-29 11:10:24 +02:00
const result = await Utils . downloadJsonAdvanced ( url , headers ) ;
2022-12-16 13:45:07 +01:00
if ( result [ "content" ] ) {
2023-09-29 11:10:24 +02:00
return result [ "content" ] ;
2022-12-15 20:24:53 +01:00
}
2023-09-29 11:10:24 +02:00
throw result [ "error" ] ;
2022-12-15 20:24:53 +01:00
}
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 } > {
2023-09-29 11:10:24 +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 ) {
2023-09-29 11:10:24 +02:00
return cached . promise ;
2021-10-14 03:46:09 +02:00
}
}
2022-12-16 13:45:07 +01:00
const promise =
/*NO AWAIT as we work with the promise directly */ Utils . downloadJsonAdvanced (
2023-09-29 11:10:24 +02:00
url ,
headers
) ;
Utils . _download_cache . set ( url , { promise , timestamp : new Date ( ) . getTime ( ) } ) ;
return await promise ;
2021-10-14 03:46:09 +02:00
}
2021-09-28 17:30:48 +02:00
public static async downloadJson ( url : string , headers? : any ) : Promise < any > {
2023-09-29 11:10:24 +02:00
const result = await Utils . downloadJsonAdvanced ( url , headers ) ;
2022-12-16 13:45:07 +01:00
if ( result [ "content" ] ) {
2023-09-29 11:10:24 +02:00
return result [ "content" ] ;
2022-12-15 20:24:53 +01:00
}
2023-09-29 11:10:24 +02:00
throw result [ "error" ] ;
2022-12-15 20:24:53 +01:00
}
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 } > {
2023-09-29 11:10:24 +02:00
const injected = Utils . injectedDownloads [ url ] ;
2021-09-28 18:40:45 +02:00
if ( injected !== undefined ) {
2023-09-29 11:10:24 +02:00
console . log ( "Using injected resource for test for URL" , url ) ;
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 ,
2023-07-27 12:47:19 +02:00
Utils . Merge ( { accept : "application/json" } , headers ? ? { } )
2023-09-29 11:10:24 +02:00
) ;
2022-12-16 13:45:07 +01:00
if ( result [ "error" ] !== undefined ) {
2023-09-29 11:10:24 +02:00
return < { error : string ; url : string ; statuscode? : number } > result ;
2022-12-15 20:24:53 +01:00
}
2023-09-29 11:10:24 +02: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" ) {
2023-09-29 11:10:24 +02:00
return { content : JSON.parse ( data ) } ;
2022-01-25 18:20:15 +01:00
}
2023-09-29 11:10:24 +02:00
return { content : data } ;
2021-09-28 18:40:45 +02:00
} catch ( e ) {
2023-09-29 11:10:24 +02:00
console . error ( "Could not parse " , data , "due to" , e , "\n" , e . stack ) ;
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"
2023-04-19 03:20:49 +02:00
| "image/png"
2022-04-18 02:39:30 +02:00
}
) {
2023-09-29 11:10:24 +02:00
const element = document . createElement ( "a" ) ;
let file ;
2021-07-27 19:35:43 +02:00
if ( typeof contents === "string" ) {
2023-09-29 11:10:24 +02:00
file = new Blob ( [ contents ] , { type : options ? . mimetype ? ? "text/plain" } ) ;
2021-07-27 19:35:43 +02:00
} else {
2023-09-29 11:10:24 +02:00
file = contents ;
2021-07-27 19:35:43 +02:00
}
2023-09-29 11:10:24 +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-07 13:17:42 +02:00
}
2021-05-18 19:48:20 +02:00
public static ColourNameToHex ( color : string ) : string {
2023-09-29 11:10:24 +02:00
return colors [ color . toLowerCase ( ) ] ? ? color ;
2021-05-11 02:39:51 +02:00
}
2021-05-18 19:48:20 +02:00
public static HexToColourName ( hex : string ) : string {
2023-09-29 11:10:24 +02:00
hex = hex . toLowerCase ( ) ;
2021-05-18 19:48:20 +02:00
if ( ! hex . startsWith ( "#" ) ) {
2023-09-29 11:10:24 +02:00
return hex ;
2021-05-11 02:39:51 +02:00
}
2023-09-29 11:10:24 +02:00
const c = Utils . color ( hex ) ;
2021-05-18 19:48:20 +02:00
2023-09-29 11:10:24 +02:00
let smallestDiff = Number . MAX_VALUE ;
let bestColor = undefined ;
2021-05-11 02:39:51 +02:00
for ( const color in colors ) {
2021-05-18 19:48:20 +02:00
if ( ! colors . hasOwnProperty ( color ) ) {
2023-09-29 11:10:24 +02:00
continue ;
2021-05-11 02:39:51 +02:00
}
2023-09-29 11:10:24 +02:00
const foundhex = colors [ color ] ;
2021-05-18 19:48:20 +02:00
if ( typeof foundhex !== "string" ) {
2023-09-29 11:10:24 +02:00
continue ;
2021-05-11 02:39:51 +02:00
}
2021-05-18 19:48:20 +02:00
if ( foundhex === hex ) {
2023-09-29 11:10:24 +02:00
return color ;
2021-05-11 02:39:51 +02:00
}
2023-09-29 11:10:24 +02:00
const diff = this . colorDiff ( Utils . color ( foundhex ) , c ) ;
2021-05-18 19:48:20 +02:00
if ( diff > 50 ) {
2023-09-29 11:10:24 +02:00
continue ;
2021-05-11 02:39:51 +02:00
}
2021-05-18 19:48:20 +02:00
if ( diff < smallestDiff ) {
2023-09-29 11:10:24 +02:00
smallestDiff = diff ;
bestColor = color ;
2021-05-11 02:39:51 +02:00
}
}
2023-09-29 11:10:24 +02:00
return bestColor ? ? hex ;
2021-05-11 02:39:51 +02:00
}
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 ) {
2023-09-29 11:10:24 +02:00
const copy = { } ;
let keys = Object . keys ( o ) ;
keys = keys . sort ( ) ;
2021-09-14 18:20:25 +02:00
for ( const key of keys ) {
2023-09-29 11:10:24 +02:00
let v = o [ key ] ;
2021-09-22 16:07:56 +02:00
if ( typeof v === "object" ) {
2023-09-29 11:10:24 +02:00
v = Utils . sortKeys ( v ) ;
2021-09-14 18:20:25 +02:00
}
2023-09-29 11:10:24 +02:00
copy [ key ] = v ;
2021-09-14 18:20:25 +02:00
}
2023-09-29 11:10:24 +02:00
return copy ;
2021-09-14 18:20:25 +02:00
}
2021-09-26 17:36:39 +02:00
public static async waitFor ( timeMillis : number ) : Promise < void > {
return new Promise ( ( resolve ) = > {
2023-09-29 11:10:24 +02:00
window . setTimeout ( resolve , timeMillis ) ;
} ) ;
2021-09-26 17:36:39 +02:00
}
2021-10-18 22:17:15 +02:00
public static toHumanTime ( seconds ) : string {
2023-09-29 11:10:24 +02:00
seconds = Math . floor ( seconds ) ;
let minutes = Math . floor ( seconds / 60 ) ;
seconds = seconds % 60 ;
let hours = Math . floor ( minutes / 60 ) ;
minutes = minutes % 60 ;
const days = Math . floor ( hours / 24 ) ;
hours = hours % 24 ;
2021-10-18 22:17:15 +02:00
if ( days > 0 ) {
2023-09-29 11:10:24 +02:00
return days + "days" + " " + hours + "h" ;
2021-10-13 01:28:46 +02:00
}
2023-09-29 11:10:24 +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" ) {
2023-09-29 11:10:24 +02:00
return ;
2022-09-08 21:40:48 +02:00
}
2023-09-29 11:10:24 +02:00
e . preventDefault ( ) ;
return false ;
2022-09-08 21:40:48 +02:00
} ,
false
2023-09-29 11:10:24 +02:00
) ;
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 ) {
2023-09-29 11:10:24 +02:00
event ? . originalEvent ? . preventDefault ( ) ;
event ? . originalEvent ? . stopPropagation ( ) ;
event ? . originalEvent ? . stopImmediatePropagation ( ) ;
2023-01-04 17:41:11 +01:00
if ( event ? . originalEvent ) {
// This is a total workaround, as 'preventDefault' and everything above seems to be not working
2023-09-29 11:10:24 +02:00
event . originalEvent [ "dismissed" ] = true ;
2023-01-04 17:41:11 +01:00
}
}
2023-04-20 17:42:07 +02:00
public static HomepageLink ( ) : string {
if ( typeof window === "undefined" ) {
2023-09-29 11:10:24 +02:00
return "https://mapcomplete.org" ;
2023-04-20 17:42:07 +02:00
}
2023-06-14 20:39:36 +02:00
const path = (
window . location . protocol +
"//" +
window . location . host +
window . location . pathname
2023-09-29 11:10:24 +02:00
) . split ( "/" ) ;
path . pop ( ) ;
path . push ( "index.html" ) ;
return path . join ( "/" ) ;
2023-04-20 17:42:07 +02:00
}
2021-10-18 22:17:15 +02:00
public static OsmChaLinkFor ( daysInThePast , theme = undefined ) : string {
2023-09-29 11:10:24 +02:00
const now = new Date ( ) ;
const lastWeek = new Date ( now . getTime ( ) - daysInThePast * 24 * 60 * 60 * 1000 ) ;
2021-10-16 18:30:24 +02:00
const date =
lastWeek . getFullYear ( ) +
"-" +
Utils . TwoDigits ( lastWeek . getMonth ( ) + 1 ) +
"-" +
2023-09-29 11:10:24 +02:00
Utils . TwoDigits ( lastWeek . getDate ( ) ) ;
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 =
2023-09-29 11:10:24 +02:00
osmcha_link + "," + ` "comment":[{"label":"# ${ theme } ","value":"# ${ theme } "}] ` ;
2021-10-16 18:30:24 +02:00
}
2023-09-29 11:10:24 +02: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 ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2022-01-07 17:31:39 +01:00
}
2023-09-29 11:10:24 +02:00
return JSON . parse ( JSON . stringify ( x ) ) ;
2022-01-07 17:31:39 +01:00
}
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" ) ) {
2023-09-29 11:10:24 +02:00
str = str . replace ( " UTC" , "+00" ) ;
2022-01-24 03:09:21 +01:00
}
2023-09-29 11:10:24 +02:00
return new Date ( str ) ;
2022-01-24 03:09:21 +01:00
}
2022-04-06 03:06:50 +02:00
2023-08-10 15:37:44 +02:00
public static selectTextIn ( node ) {
if ( document . body [ "createTextRange" ] ) {
2023-09-29 11:10:24 +02:00
const range = document . body [ "createTextRange" ] ( ) ;
range . moveToElementText ( node ) ;
range . select ( ) ;
2023-08-10 15:37:44 +02:00
} else if ( window . getSelection ) {
2023-09-29 11:10:24 +02:00
const selection = window . getSelection ( ) ;
const range = document . createRange ( ) ;
range . selectNodeContents ( node ) ;
selection . removeAllRanges ( ) ;
selection . addRange ( range ) ;
2023-08-10 15:37:44 +02:00
} else {
2023-09-29 11:10:24 +02:00
console . warn ( "Could not select text in node: Unsupported browser." ) ;
2023-08-10 15:37:44 +02:00
}
}
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 ,
2023-09-29 11:10:24 +02:00
Utils . levenshteinDistance ( getName ( t ) , reference )
] ) ;
withDistance . sort ( ( [ _ , a ] , [ __ , b ] ) = > a - b ) ;
return withDistance . map ( ( n ) = > n [ 0 ] ) ;
2022-03-29 00:20:10 +02:00
}
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 )
2023-09-29 11:10:24 +02:00
. map ( ( ) = > Array ( str1 . length + 1 ) . fill ( null ) ) ;
2022-02-19 02:45:15 +01:00
for ( let i = 0 ; i <= str1 . length ; i += 1 ) {
2023-09-29 11:10:24 +02:00
track [ 0 ] [ i ] = i ;
2022-02-19 02:45:15 +01:00
}
for ( let j = 0 ; j <= str2 . length ; j += 1 ) {
2023-09-29 11:10:24 +02:00
track [ j ] [ 0 ] = j ;
2022-02-19 02:45:15 +01:00
}
for ( let j = 1 ; j <= str2 . length ; j += 1 ) {
for ( let i = 1 ; i <= str1 . length ; i += 1 ) {
2023-09-29 11:10:24 +02:00
const indicator = str1 [ i - 1 ] === str2 [ j - 1 ] ? 0 : 1 ;
2022-02-19 02:45:15 +01:00
track [ j ] [ i ] = Math . min (
track [ j ] [ i - 1 ] + 1 , // deletion
track [ j - 1 ] [ i ] + 1 , // insertion
track [ j - 1 ] [ i - 1 ] + indicator // substitution
2023-09-29 11:10:24 +02:00
) ;
2022-02-19 02:45:15 +01:00
}
}
2023-09-29 11:10:24 +02:00
return track [ str2 . length ] [ str1 . length ] ;
2022-02-19 02:45:15 +01:00
}
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 > {
2023-09-29 11:10:24 +02:00
const o = { } ;
const keys = Array . from ( d . keys ( ) ) ;
keys . sort ( ) ;
2022-04-22 16:51:49 +02:00
for ( const key of keys ) {
2023-09-29 11:10:24 +02:00
o [ key ] = onValue ( d . get ( key ) , key ) ;
2022-04-22 16:51:49 +02:00
}
2023-09-29 11:10:24 +02:00
return o ;
2022-03-18 01:21:00 +01:00
}
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 [ ] > {
2023-09-29 11:10:24 +02:00
const newD : Record < V , K [ ] > = < any > { } ;
2022-06-24 16:47:00 +02:00
for ( const k in d ) {
2023-09-29 11:10:24 +02:00
const vs = d [ k ] ;
2023-07-27 14:41:55 +02:00
for ( const v of vs ) {
2023-09-29 11:10:24 +02:00
const list = newD [ v ] ;
2022-06-24 16:47:00 +02:00
if ( list === undefined ) {
2023-09-29 11:10:24 +02:00
newD [ v ] = [ k ] ; // Left: indexing; right: list with one element
2022-06-24 16:47:00 +02:00
} else {
2023-09-29 11:10:24 +02:00
list . push ( k ) ;
2022-06-24 16:47:00 +02:00
}
}
}
2023-09-29 11:10:24 +02:00
return newD ;
2022-06-24 16:47:00 +02:00
}
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 ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2022-03-18 01:21:00 +01:00
}
2022-04-06 03:06:50 +02:00
2022-03-18 01:21:00 +01:00
function componentToHex ( n ) {
2023-09-29 11:10:24 +02:00
const 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
2023-09-29 11:10:24 +02:00
return "#" + componentToHex ( c . r ) + componentToHex ( c . g ) + componentToHex ( c . b ) ;
2022-03-18 01:21:00 +01:00
}
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 ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2022-03-18 01:21:00 +01:00
}
2023-09-29 11:10:24 +02:00
hex = hex . replace ( /[ \t]/g , "" ) ;
2022-03-18 01:21:00 +01:00
if ( hex . startsWith ( "rgba(" ) ) {
2023-09-29 11:10:24 +02:00
const match = hex . match ( /rgba\(([0-9.]+),([0-9.]+),([0-9.]+)(,[0-9.]*)?\)/ ) ;
2022-04-06 03:06:50 +02:00
if ( match == undefined ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2022-03-18 01:21:00 +01:00
}
2023-09-29 11:10:24 +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 ( "#" ) ) {
2023-09-29 11:10:24 +02:00
return undefined ;
2021-10-18 22:17:15 +02:00
}
if ( hex . length === 4 ) {
return {
r : parseInt ( hex . substr ( 1 , 1 ) , 16 ) ,
g : parseInt ( hex . substr ( 2 , 1 ) , 16 ) ,
2023-09-29 11:10:24 +02:00
b : parseInt ( hex . substr ( 3 , 1 ) , 16 )
} ;
2021-10-18 22:17:15 +02:00
}
return {
r : parseInt ( hex . substr ( 1 , 2 ) , 16 ) ,
g : parseInt ( hex . substr ( 3 , 2 ) , 16 ) ,
2023-09-29 11:10:24 +02:00
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 > {
2023-09-29 11:10:24 +02:00
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 ) {
2023-09-29 11:10:24 +02:00
d . set ( tag . key , tag . value ) ;
2022-03-23 19:48:06 +01:00
}
2022-03-18 01:21:00 +01:00
2023-09-29 11:10:24 +02:00
return d ;
2022-03-23 19:48:06 +01:00
}
2022-04-06 03:06:50 +02:00
2022-07-25 18:55:15 +02:00
static toIdRecord < T extends { id : string } > ( ts : T [ ] ) : Record < string , T > {
2023-09-29 11:10:24 +02:00
const result : Record < string , T > = { } ;
2022-07-25 18:55:15 +02:00
for ( const t of ts ) {
2023-09-29 11:10:24 +02:00
result [ t . id ] = t ;
2022-07-25 18:55:15 +02:00
}
2023-09-29 11:10:24 +02:00
return result ;
2022-07-25 18:55:15 +02:00
}
2022-09-08 21:40:48 +02:00
2022-08-18 23:37:44 +02:00
public static SetMidnight ( d : Date ) : void {
2023-09-29 11:10:24 +02:00
d . setUTCHours ( 0 ) ;
d . setUTCSeconds ( 0 ) ;
d . setUTCMilliseconds ( 0 ) ;
d . setUTCMinutes ( 0 ) ;
2022-08-18 23:37:44 +02:00
}
2022-12-06 03:43:54 +01:00
2023-05-22 01:37:02 +02:00
public static scrollIntoView ( element : HTMLBaseElement ) {
2023-05-06 01:23:55 +02:00
// Is the element completely in the view?
2023-09-29 11:10:24 +02:00
const parentRect = Utils . findParentWithScrolling ( element ) . getBoundingClientRect ( ) ;
const elementRect = element . getBoundingClientRect ( ) ;
2023-05-06 01:23:55 +02:00
// Check if the element is within the vertical bounds of the parent element
2023-09-29 11:10:24 +02:00
const topIsVisible = elementRect . top >= parentRect . top ;
const bottomIsVisible = elementRect . bottom <= parentRect . bottom ;
const inView = topIsVisible && bottomIsVisible ;
2023-05-06 01:23:55 +02:00
if ( inView ) {
2023-09-29 11:10:24 +02:00
return ;
2023-05-06 01:23:55 +02:00
}
2023-09-29 11:10:24 +02:00
element . scrollIntoView ( { behavior : "smooth" , block : "nearest" } ) ;
2023-05-06 01:23:55 +02:00
}
2023-05-22 01:37:02 +02:00
2023-05-06 01:23:55 +02:00
public static findParentWithScrolling ( element : HTMLBaseElement ) : HTMLBaseElement {
2022-12-06 03:43:54 +01:00
// Check if the element itself has scrolling
if ( element . scrollHeight > element . clientHeight ) {
2023-09-29 11:10:24 +02: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 ) {
2023-09-29 11:10:24 +02:00
return null ;
2022-12-06 03:43:54 +01:00
}
// If the element has a parent, repeat the process for the parent element
2023-09-29 11:10:24 +02:00
return Utils . findParentWithScrolling ( < HTMLBaseElement > element . parentElement ) ;
2022-12-06 03:43:54 +01:00
}
2023-03-28 05:13:48 +02:00
/ * *
* Returns true if the contents of ` a ` are the same ( and in the same order ) as ` b ` .
* Might have false negatives in some cases
* @param a
* @param b
* /
public static sameList < T > ( a : ReadonlyArray < T > , b : ReadonlyArray < T > ) {
if ( a == b ) {
2023-09-29 11:10:24 +02:00
return true ;
2023-03-28 05:13:48 +02:00
}
if ( a === undefined || a === null || b === undefined || b === null ) {
2023-09-29 11:10:24 +02:00
return false ;
2023-03-28 05:13:48 +02:00
}
if ( a . length !== b . length ) {
2023-09-29 11:10:24 +02:00
return false ;
2023-03-28 05:13:48 +02:00
}
for ( let i = 0 ; i < a . length ; i ++ ) {
2023-09-29 11:10:24 +02:00
const ai = a [ i ] ;
const bi = b [ i ] ;
2023-03-28 05:13:48 +02:00
if ( ai == bi ) {
2023-09-29 11:10:24 +02:00
continue ;
2023-03-28 05:13:48 +02:00
}
if ( ai === bi ) {
2023-09-29 11:10:24 +02:00
continue ;
2023-03-28 05:13:48 +02:00
}
2023-09-29 11:10:24 +02:00
return false ;
2023-03-28 05:13:48 +02:00
}
2023-09-29 11:10:24 +02:00
return true ;
2023-03-28 05:13:48 +02:00
}
2023-04-20 18:58:31 +02:00
2023-06-14 20:39:36 +02:00
public static SameObject ( a : any , b : any ) {
2023-04-20 18:58:31 +02:00
if ( a === b ) {
2023-09-29 11:10:24 +02:00
return true ;
2023-04-20 18:58:31 +02:00
}
if ( a === undefined || a === null || b === null || b === undefined ) {
2023-09-29 11:10:24 +02:00
return false ;
2023-04-20 18:58:31 +02:00
}
if ( typeof a === "object" && typeof b === "object" ) {
for ( const aKey in a ) {
if ( ! ( aKey in b ) ) {
2023-09-29 11:10:24 +02:00
return false ;
2023-04-20 18:58:31 +02:00
}
}
for ( const bKey in b ) {
if ( ! ( bKey in a ) ) {
2023-09-29 11:10:24 +02:00
return false ;
2023-04-20 18:58:31 +02:00
}
}
for ( const k in a ) {
if ( ! Utils . SameObject ( a [ k ] , b [ k ] ) ) {
2023-09-29 11:10:24 +02:00
return false ;
2023-04-20 18:58:31 +02:00
}
}
2023-09-29 11:10:24 +02:00
return true ;
2023-04-20 18:58:31 +02:00
}
2023-09-29 11:10:24 +02:00
return false ;
2023-04-20 18:58:31 +02:00
}
2023-05-22 01:37:02 +02:00
2023-05-24 00:50:27 +02:00
/ * *
*
* Utils . splitIntoSubstitutionParts ( "abc" ) // => [{message: "abc"}]
* Utils . splitIntoSubstitutionParts ( "abc {search} def" ) // => [{message: "abc "}, {subs: "search"}, {message: " def"}]
*
* /
2023-06-14 20:39:36 +02:00
public static splitIntoSubstitutionParts (
template : string
) : ( { message : string } | { subs : string } ) [ ] {
2023-09-29 11:10:24 +02:00
const preparts = template . split ( "{" ) ;
const spec : ( { message : string } | { subs : string } ) [ ] = [ ] ;
2023-05-24 00:50:27 +02:00
for ( const prepart of preparts ) {
2023-09-29 11:10:24 +02:00
const postParts = prepart . split ( "}" ) ;
2023-06-14 20:39:36 +02:00
if ( postParts . length === 1 ) {
2023-05-24 00:50:27 +02:00
// This was a normal part
2023-09-29 11:10:24 +02:00
spec . push ( { message : postParts [ 0 ] } ) ;
2023-06-14 20:39:36 +02:00
} else {
2023-09-29 11:10:24 +02:00
const [ subs , message ] = postParts ;
spec . push ( { subs } ) ;
2023-06-14 20:39:36 +02:00
if ( message !== "" ) {
2023-09-29 11:10:24 +02:00
spec . push ( { message } ) ;
2023-05-24 00:50:27 +02:00
}
}
}
2023-09-29 11:10:24 +02:00
return spec ;
2023-05-24 00:50:27 +02:00
}
2023-07-17 01:07:01 +02:00
2023-07-27 12:47:19 +02:00
/ * *
* Returns the file and line number of the code calling this
* /
public static getLocationInCode ( offset : number = 0 ) : {
path : string
line : number
column : number
markdownLocation : string
filename : string
functionName : string
} {
2023-09-29 11:10:24 +02:00
const error = new Error ( "No error" ) ;
const stack = error . stack . split ( "\n" ) ;
stack . shift ( ) ; // Remove "Error: No error"
const regex = /at (.*) \(([a-zA-Z0-9/.]+):([0-9]+):([0-9]+)\)/ ;
const stackItem = stack [ Math . abs ( offset ) + 1 ] ;
let functionName : string ;
let path : string ;
let line : string ;
let column : string ;
let _ : string ;
const matchWithFuncName = stackItem . match ( regex ) ;
2023-07-27 12:47:19 +02:00
if ( matchWithFuncName ) {
2023-09-29 11:10:24 +02:00
; [ _ , functionName , path , line , column ] = matchWithFuncName ;
2023-07-27 12:47:19 +02:00
} else {
2023-07-27 14:41:55 +02:00
const regexNoFuncName : RegExp = new RegExp ( "at ([a-zA-Z0-9/.]+):([0-9]+):([0-9]+)" )
2023-09-29 11:10:24 +02:00
; [ _ , path , line , column ] = stackItem . match ( regexNoFuncName ) ;
2023-07-27 12:47:19 +02:00
}
2023-09-29 11:10:24 +02:00
const markdownLocation = path . substring ( path . indexOf ( "MapComplete/src" ) + 11 ) + "#L" + line ;
2023-07-27 12:47:19 +02:00
return {
path ,
functionName ,
line : Number ( line ) ,
column : Number ( column ) ,
markdownLocation ,
2023-09-29 11:10:24 +02:00
filename : path.substring ( path . lastIndexOf ( "/" ) + 1 )
} ;
2023-07-27 12:47:19 +02:00
}
2023-07-17 01:07:01 +02:00
private static colorDiff (
c0 : { r : number ; g : number ; b : number } ,
c1 : { r : number ; g : number ; b : number }
) {
2023-09-29 11:10:24 +02:00
return Math . abs ( c0 . r - c1 . r ) + Math . abs ( c0 . g - c1 . g ) + Math . abs ( c0 . b - c1 . b ) ;
2023-07-17 01:07:01 +02:00
}
2021-05-06 03:03:54 +02:00
}