forked from MapComplete/MapComplete
		
	Scripts: create script to import layers from studio into official mapcomplete
This commit is contained in:
		
							parent
							
								
									c7b905d1fb
								
							
						
					
					
						commit
						db685dc05f
					
				
					 5 changed files with 186 additions and 81 deletions
				
			
		|  | @ -2,7 +2,7 @@ | |||
|  * Script to download images from Wikimedia Commons, and save them together with license information. | ||||
|  */ | ||||
| 
 | ||||
| import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs" | ||||
| import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" | ||||
| import { unescape } from "querystring" | ||||
| import SmallLicense from "../src/Models/smallLicense" | ||||
| 
 | ||||
|  | @ -123,7 +123,7 @@ const templateMapping = { | |||
|     "Template:CC0": "CC0 1.0", | ||||
| } | ||||
| 
 | ||||
| async function main(args: string[]) { | ||||
| export async function main(args: string[]) { | ||||
|     if (args.length < 2) { | ||||
|         console.log("Usage: downloadCommons.ts <output folder> <url> <?url> <?url> .. ") | ||||
|         console.log( | ||||
|  | @ -345,4 +345,4 @@ function removeLinks(text: string): string { | |||
|     return text.replace(/<a.*?>(.*?)<\/a>/g, "$1") | ||||
| } | ||||
| 
 | ||||
| main(process.argv.slice(2)) | ||||
| // main(process.argv.slice(2))
 | ||||
|  |  | |||
							
								
								
									
										73
									
								
								scripts/importCustomTheme.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								scripts/importCustomTheme.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,73 @@ | |||
| import Script from "./Script" | ||||
| import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" | ||||
| import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson" | ||||
| import LayerConfig from "../src/Models/ThemeConfig/LayerConfig" | ||||
| import { DoesImageExist } from "../src/Models/ThemeConfig/Conversion/Validation" | ||||
| import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages" | ||||
| import { ThemeConfigJson } from "../src/Models/ThemeConfig/Json/ThemeConfigJson" | ||||
| import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext" | ||||
| import Constants from "../src/Models/Constants" | ||||
| import { main as downloadCommons } from "./downloadCommons" | ||||
| import { WikimediaImageProvider } from "../src/Logic/ImageProviders/WikimediaImageProvider" | ||||
| import { Utils } from "../src/Utils" | ||||
| import { GenerateLicenseInfo } from "./generateLicenseInfo" | ||||
| 
 | ||||
| class ImportCustomTheme extends Script { | ||||
|     constructor() { | ||||
|         super("Given the path of a custom layer, will load the layer into mapcomplete as official") | ||||
|     } | ||||
| 
 | ||||
|     async main(args: string[]) { | ||||
|         const path = args[0] | ||||
| 
 | ||||
|         const layerconfig = <LayerConfigJson>JSON.parse(readFileSync(path, "utf-8")) | ||||
|         const id = layerconfig.id | ||||
|         const dirPath = "./assets/layers/" + id | ||||
|         if (!existsSync(dirPath)) { | ||||
|             mkdirSync(dirPath) | ||||
|         } | ||||
|         const imageFinder = new ExtractImages(true) | ||||
|         const theme: ThemeConfigJson = <ThemeConfigJson>{ | ||||
|             layers: [layerconfig], | ||||
|             id: "dummy-theme-during-import", | ||||
|             title: { | ||||
|                 en: "Dummy", | ||||
|             }, | ||||
|             icon: "./assets/svg/plus.svg", | ||||
|             description: { | ||||
|                 en: "Dummy", | ||||
|             }, | ||||
|         } | ||||
|         const usedImagesAll: { | ||||
|             path: string; | ||||
|             context: string, | ||||
|             location: (string | number)[] | ||||
|         }[] = imageFinder.convert(theme, ConversionContext.construct([], ["checking images"])) | ||||
|         const usedImages = usedImagesAll.filter(img => !img.path.startsWith("./assets")) | ||||
|             .filter(img => Constants.defaultPinIcons.indexOf(img.path) < 0) | ||||
|         console.log(usedImages) | ||||
|         const wm = WikimediaImageProvider.singleton | ||||
|         for (const usedImage of usedImages) { | ||||
|             if (usedImage.path.indexOf("wikimedia") >= 0) { | ||||
|                 const filename = WikimediaImageProvider.extractFileName(usedImage.path) | ||||
|                 console.log("Canonical URL is", filename) | ||||
|                 await downloadCommons([dirPath, "https://commons.wikimedia.org/wiki/File:"+filename]) | ||||
|                 console.log("Used image context:", usedImage.context) | ||||
|                 const replaceAt = (usedImage.location) | ||||
|                     .slice(2) // We drop ".layer.0" as we put this in a dummy theme
 | ||||
|                     .map( | ||||
|                     breadcrumb => isNaN(Number(breadcrumb)) ? breadcrumb : Number(breadcrumb), | ||||
|                 ) | ||||
|                 console.log("Replacement target location is", replaceAt) | ||||
|                 Utils.WalkPath(replaceAt, layerconfig, (_, path) => { | ||||
|                     console.log("Found",path, "to replace with", filename,"origvalue:", _) | ||||
|                     return `./assets/layers/${id}/${filename}` | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|         writeFileSync("./assets/layers/"+id+"/"+id+".json", JSON.stringify(layerconfig, null, "  ")) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| new ImportCustomTheme().run() | ||||
| new GenerateLicenseInfo().run() | ||||
|  | @ -43,6 +43,7 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|         if (filename.startsWith("File:")) { | ||||
|             filename = filename.substring(5) | ||||
|         } | ||||
| 
 | ||||
|         return filename.trim().replace(/\s+/g, "_") | ||||
|     } | ||||
| 
 | ||||
|  | @ -51,10 +52,14 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|      * WikimediaImageProvider.extractFileName("https://commons.wikimedia.org/wiki/File:Somefile.jpg") // => "Somefile.jpg"
 | ||||
|      * WikimediaImageProvider.extractFileName("https://commons.wikimedia.org/wiki/File:S%C3%A8vres%20-%20square_madame_de_Pompadour_-_bo%C3%AEte_%C3%A0_livres.jpg?uselang=en") // => "Sèvres_-_square_madame_de_Pompadour_-_boîte_à_livres.jpg"
 | ||||
|      */ | ||||
|     private static extractFileName(url: string) { | ||||
|     public static extractFileName(url: string) { | ||||
|         if (!url.startsWith("http")) { | ||||
|             return url | ||||
|         } | ||||
|         const thumbMatch = url.match(this.thumbUrlRegex) | ||||
|         if (thumbMatch) { | ||||
|             return thumbMatch[1] | ||||
|         } | ||||
|         const path = decodeURIComponent(new URL(url).pathname) | ||||
|         return WikimediaImageProvider.makeCanonical(path.substring(path.lastIndexOf("/") + 1)) | ||||
|     } | ||||
|  | @ -76,16 +81,17 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|         return WikimediaImageProvider.commonsPrefixes.some((prefix) => value.startsWith(prefix)) | ||||
|     } | ||||
| 
 | ||||
|     private static readonly thumbUrlRegex = /^https:\/\/upload.wikimedia.org\/.*\/([^/]+)\/.*-\1/ | ||||
|     private static removeCommonsPrefix(value: string): string { | ||||
| 
 | ||||
|         if (value.startsWith("https://upload.wikimedia.org/")) { | ||||
|             value = value.substring(value.lastIndexOf("/") + 1) | ||||
|             value = decodeURIComponent(value) | ||||
|             if (!value.startsWith("File:")) { | ||||
|                 value = "File:" + value | ||||
|             } | ||||
|             return value | ||||
|         } | ||||
| 
 | ||||
|         if (value.startsWith("File:")) { | ||||
|             return value | ||||
|         } | ||||
|         for (const prefix of WikimediaImageProvider.commonsPrefixes) { | ||||
|             if (value.startsWith(prefix)) { | ||||
|                 let part = value.substr(prefix.length) | ||||
|  | @ -139,6 +145,9 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|      * | ||||
|      * const result = await WikimediaImageProvider.singleton.ExtractUrls("wikimedia_commons", "File:Sèvres_-_square_madame_de_Pompadour_-_boîte_à_livres.jpg") | ||||
|      * result[0].url_hd // => "https://commons.wikimedia.org/wiki/Special:FilePath/File%3AS%C3%A8vres_-_square_madame_de_Pompadour_-_bo%C3%AEte_%C3%A0_livres.jpg"
 | ||||
|      * | ||||
|      * const result = await WikimediaImageProvider.singleton.ExtractUrls("image", "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Edinburgh_City_police_box_001.jpg/576px-Edinburgh_City_police_box_001.jpg") | ||||
|      * result[0].url_hd // => "https://commons.wikimedia.org/wiki/File:Edinburgh_City_police_box_001.jpg"
 | ||||
|      */ | ||||
|     public async ExtractUrls(key: string, value: string): undefined | Promise<ProvidedImage[]> { | ||||
|         const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value) | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ import { ConversionContext } from "./ConversionContext" | |||
| 
 | ||||
| export class ExtractImages extends Conversion< | ||||
|     ThemeConfigJson, | ||||
|     { path: string; context: string }[] | ||||
|     { path: string; context: string; location: (string|number)[] }[] | ||||
| > { | ||||
|     private static readonly layoutMetaPaths = metapaths.filter((mp) => { | ||||
|         const typeHint = mp.hints.typehint | ||||
|  | @ -24,11 +24,11 @@ export class ExtractImages extends Conversion< | |||
|         ) | ||||
|     }) | ||||
|     private static readonly tagRenderingMetaPaths = tagrenderingmetapaths | ||||
|     private _isOfficial: boolean | ||||
|     private readonly _isOfficial: boolean | ||||
|     private _sharedTagRenderings: Set<string> | ||||
| 
 | ||||
|     constructor(isOfficial: boolean, sharedTagRenderings: Set<string>) { | ||||
|         super("ExctractImages", "Extract all images from a layoutConfig using the meta paths.", []) | ||||
|     constructor(isOfficial: boolean, sharedTagRenderings: Set<string> = new Set()) { | ||||
|         super("ExtractImages", "Extract all images from a themeConfig using the meta paths.") | ||||
|         this._isOfficial = isOfficial | ||||
|         this._sharedTagRenderings = sharedTagRenderings | ||||
|     } | ||||
|  | @ -111,8 +111,8 @@ export class ExtractImages extends Conversion< | |||
|     convert( | ||||
|         json: ThemeConfigJson, | ||||
|         context: ConversionContext | ||||
|     ): { path: string; context: string }[] { | ||||
|         const allFoundImages: { path: string; context: string }[] = [] | ||||
|     ): { path: string; context: string, location: (string | number)[] }[] { | ||||
|         const allFoundImages: { path: string; context: string, location: (string | number)[] }[] = [] | ||||
|         for (const metapath of ExtractImages.layoutMetaPaths) { | ||||
|             const mightBeTr = ExtractImages.mightBeTagRendering(<any>metapath) | ||||
| 
 | ||||
|  | @ -143,7 +143,7 @@ export class ExtractImages extends Conversion< | |||
|                             continue | ||||
|                         } | ||||
| 
 | ||||
|                         allFoundImages.push({ path: foundImage, context: context + "." + path }) | ||||
|                         allFoundImages.push({ path: foundImage, context: context + "." + path, location: path }) | ||||
|                     } else { | ||||
|                         // This is a tagRendering.
 | ||||
|                         // Either every rendered value might be an icon
 | ||||
|  | @ -177,6 +177,7 @@ export class ExtractImages extends Conversion< | |||
|                                         allFoundImages.push({ | ||||
|                                             path: img.leaf, | ||||
|                                             context: context + "." + path, | ||||
|                                             location: img.path | ||||
|                                         }) | ||||
|                                     } | ||||
|                                 } | ||||
|  | @ -191,6 +192,7 @@ export class ExtractImages extends Conversion< | |||
|                                             .map((path) => ({ | ||||
|                                                 path, | ||||
|                                                 context: context + "." + path, | ||||
|                                                 location:img.path | ||||
|                                             })) | ||||
|                                     ) | ||||
|                                 } | ||||
|  | @ -211,12 +213,13 @@ export class ExtractImages extends Conversion< | |||
|                     allFoundImages.push({ | ||||
|                         context: context.path.join(".") + "." + foundElement.path.join("."), | ||||
|                         path: foundElement.leaf, | ||||
|                         location: foundElement.path | ||||
|                     }) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const cleanedImages: { path: string; context: string }[] = [] | ||||
|         const cleanedImages: { path: string; context: string, location: (string | number)[] }[] = [] | ||||
| 
 | ||||
|         for (const foundImage of allFoundImages) { | ||||
|             if (foundImage.path.startsWith("<") && foundImage.path.endsWith(">")) { | ||||
|  | @ -225,7 +228,8 @@ export class ExtractImages extends Conversion< | |||
|                 const images = Array.from(doc.getElementsByTagName("img")) | ||||
|                 const paths = images.map((i) => i.getAttribute("src")) | ||||
|                 cleanedImages.push( | ||||
|                     ...paths.map((path) => ({ path, context: foundImage.context + " (in html)" })) | ||||
|                     ...paths.map((path) => ({ path, context: foundImage.context + " (in html)", | ||||
|                         location: foundImage.location })) | ||||
|                 ) | ||||
|                 continue | ||||
|             } | ||||
|  | @ -242,7 +246,7 @@ export class ExtractImages extends Conversion< | |||
|                 ) | ||||
|             ) | ||||
|             for (const path of allPaths) { | ||||
|                 cleanedImages.push({ path, context: foundImage.context }) | ||||
|                 cleanedImages.push({ path, context: foundImage.context , location:foundImage.location}) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -255,9 +259,8 @@ export class FixImages extends DesugaringStep<ThemeConfigJson> { | |||
| 
 | ||||
|     constructor(knownImages: Set<string>) { | ||||
|         super( | ||||
|             "fixImages", | ||||
|             "Walks over the entire theme and replaces images to the relative URL. Only works if the ID of the theme is an URL", | ||||
|             [], | ||||
|             "fixImages" | ||||
|         ) | ||||
|         this._knownImages = knownImages | ||||
|     } | ||||
|  |  | |||
							
								
								
									
										142
									
								
								src/Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										142
									
								
								src/Utils.ts
									
										
									
									
									
								
							|  | @ -9,7 +9,7 @@ export class Utils { | |||
|     public static runningFromConsole = typeof window === "undefined" | ||||
|     public static externalDownloadFunction: ( | ||||
|         url: string, | ||||
|         headers?: any | ||||
|         headers?: any, | ||||
|     ) => Promise<{ content: string } | { redirect: string }> | ||||
|     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.
 | ||||
|  | @ -61,7 +61,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         if (Utils.runningFromConsole) { | ||||
|             return | ||||
|         } | ||||
|         DOMPurify.addHook("afterSanitizeAttributes", function (node) { | ||||
|         DOMPurify.addHook("afterSanitizeAttributes", function(node) { | ||||
|             // set all elements owning target to target=_blank + add noopener noreferrer
 | ||||
|             const target = node.getAttribute("target") | ||||
|             if (target) { | ||||
|  | @ -83,7 +83,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|      */ | ||||
|     public static ParseVisArgs<T = Record<string, string>>( | ||||
|         specs: { name: string; defaultValue?: string }[], | ||||
|         args: string[] | ||||
|         args: string[], | ||||
|     ): T { | ||||
|         const parsed: Record<string, string> = {} | ||||
|         if (args.length > specs.length) { | ||||
|  | @ -238,7 +238,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         object: any, | ||||
|         name: string, | ||||
|         init: () => any, | ||||
|         whenDone?: () => void | ||||
|         whenDone?: () => void, | ||||
|     ) { | ||||
|         Object.defineProperty(object, name, { | ||||
|             enumerable: false, | ||||
|  | @ -266,7 +266,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         object: any, | ||||
|         name: string, | ||||
|         init: () => Promise<any>, | ||||
|         whenDone?: () => void | ||||
|         whenDone?: () => void, | ||||
|     ) { | ||||
|         Object.defineProperty(object, name, { | ||||
|             enumerable: false, | ||||
|  | @ -334,7 +334,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|      */ | ||||
|     public static DedupOnId<T = { id: string }>( | ||||
|         arr: T[], | ||||
|         toKey?: (t: T) => string | string[] | ||||
|         toKey?: (t: T) => string | string[], | ||||
|     ): T[] { | ||||
|         const uniq: T[] = [] | ||||
|         const seen = new Set<string>() | ||||
|  | @ -461,7 +461,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|     public static SubstituteKeys( | ||||
|         txt: string | undefined, | ||||
|         tags: Record<string, any> | undefined, | ||||
|         useLang?: string | ||||
|         useLang?: string, | ||||
|     ): string | undefined { | ||||
|         if (txt === undefined) { | ||||
|             return undefined | ||||
|  | @ -493,7 +493,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|                         "SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is", | ||||
|                         key, | ||||
|                         "\nThe value is", | ||||
|                         v | ||||
|                         v, | ||||
|                     ) | ||||
|                     v = v.InnerConstructElement()?.textContent | ||||
|                 } | ||||
|  | @ -614,7 +614,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|                     if (!Array.isArray(targetV)) { | ||||
|                         throw new Error( | ||||
|                             "Cannot concatenate: value to add is not an array: " + | ||||
|                                 JSON.stringify(targetV) | ||||
|                             JSON.stringify(targetV), | ||||
|                         ) | ||||
|                     } | ||||
|                     if (Array.isArray(sourceV)) { | ||||
|  | @ -622,9 +622,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|                     } else { | ||||
|                         throw new Error( | ||||
|                             "Could not merge concatenate " + | ||||
|                                 JSON.stringify(sourceV) + | ||||
|                                 " and " + | ||||
|                                 JSON.stringify(targetV) | ||||
|                             JSON.stringify(sourceV) + | ||||
|                             " and " + | ||||
|                             JSON.stringify(targetV), | ||||
|                         ) | ||||
|                     } | ||||
|                 } else { | ||||
|  | @ -660,16 +660,19 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|     /** | ||||
|      * Walks the specified path into the object till the end. | ||||
|      * | ||||
|      * If a list is encountered, this is transparently walked recursively on every object. | ||||
|      * If a list is encountered, the behaviour depends on the next breadcrumb: | ||||
|      * - if it is a string, the list is transparently walked recursively on every object. | ||||
|      * - it is is a number, that index will be taken | ||||
|      * | ||||
|      * If 'null' or 'undefined' is encountered, this method stops | ||||
|      * | ||||
|      * The leaf objects are replaced in the object itself by the specified function. | ||||
|      */ | ||||
|     public static WalkPath( | ||||
|         path: string[], | ||||
|         path: (string | number)[], | ||||
|         object: any, | ||||
|         replaceLeaf: (leaf: any, travelledPath: string[]) => any, | ||||
|         travelledPath: string[] = [] | ||||
|         travelledPath: (string)[] = [], | ||||
|     ): void { | ||||
|         if (object == null) { | ||||
|             return | ||||
|  | @ -699,12 +702,28 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|             return | ||||
|         } | ||||
|         if (Array.isArray(sub)) { | ||||
|             sub.forEach((el, i) => | ||||
|                 Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, head, "" + i]) | ||||
|             ) | ||||
|             if (typeof path[1] === "number") { | ||||
|                 const i = path[1] | ||||
|                 if(path.length == 2){ | ||||
|                     // We found the leaf
 | ||||
|                     const leaf = sub[i] | ||||
|                     if (leaf !== undefined) { | ||||
|                         sub[i] = replaceLeaf(leaf, [...travelledPath, ""+head,""+i]) | ||||
|                         if (sub[i] === undefined) { | ||||
|                             delete sub[i] | ||||
|                         } | ||||
|                     } | ||||
|                 }else{ | ||||
|                     Utils.WalkPath(path.slice(2), sub[i], replaceLeaf, [...travelledPath, "" + head, "" + i]) | ||||
|                 } | ||||
|             } else { | ||||
|                 sub.forEach((el, i) => | ||||
|                     Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, "" + head, "" + i]), | ||||
|                 ) | ||||
|             } | ||||
|             return | ||||
|         } | ||||
|         Utils.WalkPath(path.slice(1), sub, replaceLeaf, [...travelledPath, head]) | ||||
|         Utils.WalkPath(path.slice(1), sub, replaceLeaf, [...travelledPath, ""+head]) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -717,7 +736,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         path: string[], | ||||
|         object: any, | ||||
|         collectedList: { leaf: any; path: string[] }[] = [], | ||||
|         travelledPath: string[] = [] | ||||
|         travelledPath: string[] = [], | ||||
|     ): { leaf: any; path: string[] }[] { | ||||
|         if (object === undefined || object === null) { | ||||
|             return collectedList | ||||
|  | @ -747,7 +766,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
| 
 | ||||
|         if (Array.isArray(sub)) { | ||||
|             sub.forEach((el, i) => | ||||
|                 Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i]) | ||||
|                 Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i]), | ||||
|             ) | ||||
|             return collectedList | ||||
|         } | ||||
|  | @ -791,7 +810,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         json: any, | ||||
|         f: (v: object | number | string | boolean | undefined, path: string[]) => any, | ||||
|         isLeaf: (object) => boolean = undefined, | ||||
|         path: string[] = [] | ||||
|         path: string[] = [], | ||||
|     ) { | ||||
|         if (json === undefined || json === null) { | ||||
|             return f(json, path) | ||||
|  | @ -830,7 +849,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         json: any, | ||||
|         collect: (v: number | string | boolean | undefined, path: string[]) => any, | ||||
|         isLeaf: (object) => boolean = undefined, | ||||
|         path = [] | ||||
|         path = [], | ||||
|     ): void { | ||||
|         if (json === undefined) { | ||||
|             return | ||||
|  | @ -875,7 +894,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
| 
 | ||||
|     public static async download( | ||||
|         url: string, | ||||
|         headers?: Record<string, string> | ||||
|         headers?: Record<string, string>, | ||||
|     ): Promise<string | undefined> { | ||||
|         const result = await Utils.downloadAdvanced(url, headers) | ||||
|         if (result["error"] !== undefined) { | ||||
|  | @ -889,7 +908,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         headers?: Record<string, string>, | ||||
|         method: "POST" | "GET" | "PUT" | "UPDATE" | "DELETE" | "OPTIONS" = "GET", | ||||
|         content?: string, | ||||
|         maxAttempts: number = 3 | ||||
|         maxAttempts: number = 3, | ||||
|     ): Promise< | ||||
|         | { content: string } | ||||
|         | { redirect: string } | ||||
|  | @ -913,7 +932,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|             console.log( | ||||
|                 `Request to ${url} failed, Trying again in a moment. Attempt ${ | ||||
|                     i + 1 | ||||
|                 }/${maxAttempts}` | ||||
|                 }/${maxAttempts}`,
 | ||||
|             ) | ||||
|             await Utils.waitFor((i + 1) * 500) | ||||
|         } | ||||
|  | @ -927,7 +946,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         url: string, | ||||
|         headers?: Record<string, string>, | ||||
|         method: "POST" | "GET" | "PUT" | "UPDATE" | "DELETE" | "OPTIONS" = "GET", | ||||
|         content?: string | ||||
|         content?: string, | ||||
|     ): Promise< | ||||
|         | { content: string } | ||||
|         | { redirect: string } | ||||
|  | @ -970,12 +989,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|             xhr.onerror = (ev: ProgressEvent<EventTarget>) => | ||||
|                 reject( | ||||
|                     "Could not get " + | ||||
|                         url + | ||||
|                         ", xhr status code is " + | ||||
|                         xhr.status + | ||||
|                         " (" + | ||||
|                         xhr.statusText + | ||||
|                         ")" | ||||
|                     url + | ||||
|                     ", xhr status code is " + | ||||
|                     xhr.status + | ||||
|                     " (" + | ||||
|                     xhr.statusText + | ||||
|                     ")", | ||||
|                 ) | ||||
|         }) | ||||
|     } | ||||
|  | @ -983,7 +1002,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|     public static upload( | ||||
|         url: string, | ||||
|         data: string | Blob, | ||||
|         headers?: Record<string, string> | ||||
|         headers?: Record<string, string>, | ||||
|     ): Promise<string> { | ||||
|         return new Promise((resolve, reject) => { | ||||
|             const xhr = new XMLHttpRequest() | ||||
|  | @ -1012,13 +1031,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         url: string, | ||||
|         maxCacheTimeMs: number, | ||||
|         headers?: Record<string, string>, | ||||
|         dontCacheErrors: boolean = false | ||||
|         dontCacheErrors: boolean = false, | ||||
|     ): Promise<T> { | ||||
|         const result = await Utils.downloadJsonCachedAdvanced( | ||||
|             url, | ||||
|             maxCacheTimeMs, | ||||
|             headers, | ||||
|             dontCacheErrors | ||||
|             dontCacheErrors, | ||||
|         ) | ||||
|         if (result["content"]) { | ||||
|             return result["content"] | ||||
|  | @ -1031,7 +1050,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         maxCacheTimeMs: number, | ||||
|         headers?: Record<string, string>, | ||||
|         dontCacheErrors = false, | ||||
|         maxAttempts = 3 | ||||
|         maxAttempts = 3, | ||||
|     ): Promise< | ||||
|         { content: T } | { error: string; url: string; statuscode?: number; errContent?: object } | ||||
|     > { | ||||
|  | @ -1043,10 +1062,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         } | ||||
|         const promise = | ||||
|             /*NO AWAIT as we work with the promise directly */ Utils.downloadJsonAdvanced<T>( | ||||
|                 url, | ||||
|                 headers, | ||||
|                 maxAttempts | ||||
|             ) | ||||
|             url, | ||||
|             headers, | ||||
|             maxAttempts, | ||||
|         ) | ||||
|         Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() }) | ||||
|         try { | ||||
|             return await promise | ||||
|  | @ -1060,12 +1079,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
| 
 | ||||
|     public static async downloadJson<T = object | []>( | ||||
|         url: string, | ||||
|         headers?: Record<string, string> | ||||
|         headers?: Record<string, string>, | ||||
|     ): Promise<T> | ||||
|     public static async downloadJson<T>(url: string, headers?: Record<string, string>): Promise<T> | ||||
|     public static async downloadJson( | ||||
|         url: string, | ||||
|         headers?: Record<string, string> | ||||
|         headers?: Record<string, string>, | ||||
|     ): Promise<object | []> { | ||||
|         const result = await Utils.downloadJsonAdvanced(url, headers) | ||||
|         if (result["content"]) { | ||||
|  | @ -1085,7 +1104,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|     public static async downloadJsonAdvanced<T = object | []>( | ||||
|         url: string, | ||||
|         headers?: Record<string, string>, | ||||
|         maxAttempts = 3 | ||||
|         maxAttempts = 3, | ||||
|     ): Promise< | ||||
|         { content: T } | { error: string; url: string; statuscode?: number; errContent?: object } | ||||
|     > { | ||||
|  | @ -1098,7 +1117,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|             Utils.Merge({ accept: "application/json" }, headers ?? {}), | ||||
|             "GET", | ||||
|             undefined, | ||||
|             maxAttempts | ||||
|             maxAttempts, | ||||
|         ) | ||||
|         if (result["error"] !== undefined) { | ||||
|             return <{ error: string; url: string; statuscode?: number }>result | ||||
|  | @ -1121,7 +1140,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|                 "due to", | ||||
|                 e, | ||||
|                 "\n", | ||||
|                 e.stack | ||||
|                 e.stack, | ||||
|             ) | ||||
|             return { error: "malformed", url } | ||||
|         } | ||||
|  | @ -1142,7 +1161,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|                 | "{gpx=application/gpx+xml}" | ||||
|                 | "application/json" | ||||
|                 | "image/png" | ||||
|         } | ||||
|         }, | ||||
|     ) { | ||||
|         const element = document.createElement("a") | ||||
|         let file | ||||
|  | @ -1263,19 +1282,19 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
| 
 | ||||
|     public static sortedByLevenshteinDistance( | ||||
|         reference: string, | ||||
|         ts: ReadonlyArray<string> | ||||
|         ts: ReadonlyArray<string>, | ||||
|     ): string[] | ||||
|     public static sortedByLevenshteinDistance<T>( | ||||
|         reference: string, | ||||
|         ts: ReadonlyArray<T>, | ||||
|         getName: (t: T) => string | ||||
|         getName: (t: T) => string, | ||||
|     ): T[] | ||||
|     public static sortedByLevenshteinDistance<T>( | ||||
|         reference: string, | ||||
|         ts: ReadonlyArray<T>, | ||||
|         getName?: (t: T) => string | ||||
|         getName?: (t: T) => string, | ||||
|     ): T[] { | ||||
|         getName ??= (str) => <string> str; | ||||
|         getName ??= (str) => <string>str | ||||
|         const withDistance: [T, number][] = ts.map((t) => [ | ||||
|             t, | ||||
|             Utils.levenshteinDistance(getName(t), reference), | ||||
|  | @ -1300,7 +1319,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|                 track[j][i] = Math.min( | ||||
|                     track[j][i - 1] + 1, // deletion
 | ||||
|                     track[j - 1][i] + 1, // insertion
 | ||||
|                     track[j - 1][i - 1] + indicator // substitution
 | ||||
|                     track[j - 1][i - 1] + indicator, // substitution
 | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|  | @ -1310,11 +1329,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|     public static MapToObj<V>(d: Map<string, V>): Record<string, V> | ||||
|     public static MapToObj<V, T>( | ||||
|         d: Map<string, V>, | ||||
|         onValue: (t: V, key: string) => T | ||||
|         onValue: (t: V, key: string) => T, | ||||
|     ): Record<string, T> | ||||
|     public static MapToObj<V, T>( | ||||
|         d: Map<string, V>, | ||||
|         onValue: (t: V, key: string) => T = undefined | ||||
|         onValue: (t: V, key: string) => T = undefined, | ||||
|     ): Record<string, T> { | ||||
|         const o = {} | ||||
|         const keys = Array.from(d.keys()) | ||||
|  | @ -1332,7 +1351,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|      * 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[]> | ||||
|         d: Record<K, V[]>, | ||||
|     ): Record<V, K[]> { | ||||
|         const newD: Record<V, K[]> = <any>{} | ||||
| 
 | ||||
|  | @ -1355,7 +1374,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|      * {"a": "b", "c":"d"} // => {"b":"a", "d":"c"}
 | ||||
|      */ | ||||
|     public static transposeMapSimple<K extends string, V extends string>( | ||||
|         d: Record<K, V> | ||||
|         d: Record<K, V>, | ||||
|     ): Record<V, K> { | ||||
|         const inv = <Record<V, K>>{} | ||||
|         for (const k in d) { | ||||
|  | @ -1438,7 +1457,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|     } | ||||
| 
 | ||||
|     public static asDict( | ||||
|         tags: { key: string; value: string | number }[] | ||||
|         tags: { key: string; value: string | number }[], | ||||
|     ): Map<string, string | number> { | ||||
|         const d = new Map<string, string | number>() | ||||
| 
 | ||||
|  | @ -1451,7 +1470,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
| 
 | ||||
|     public static asRecord<K extends string | number | symbol, V>( | ||||
|         keys: K[], | ||||
|         f: (k: K) => V | ||||
|         f: (k: K) => V, | ||||
|     ): Record<K, V> { | ||||
|         const results = <Record<K, V>>{} | ||||
|         for (const key of keys) { | ||||
|  | @ -1564,7 +1583,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|      * | ||||
|      */ | ||||
|     public static splitIntoSubstitutionParts( | ||||
|         template: string | ||||
|         template: string, | ||||
|     ): ({ message: string } | { subs: string })[] { | ||||
|         const preparts = template.split("{") | ||||
|         const spec: ({ message: string } | { subs: string })[] = [] | ||||
|  | @ -1738,7 +1757,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|     } | ||||
| 
 | ||||
|     private static findParentWithScrolling( | ||||
|         element: HTMLBaseElement | HTMLDivElement | ||||
|         element: HTMLBaseElement | HTMLDivElement, | ||||
|     ): HTMLBaseElement | HTMLDivElement { | ||||
|         // Check if the element itself has scrolling
 | ||||
|         if (element.scrollHeight > element.clientHeight) { | ||||
|  | @ -1820,6 +1839,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         href = href.replaceAll(/ /g, "%20") | ||||
|         return href | ||||
|     } | ||||
| 
 | ||||
|     private static emojiRegex = /[\p{Extended_Pictographic}🛰️]/u | ||||
| 
 | ||||
|     /** | ||||
|  | @ -1855,6 +1875,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         for (const key of allKeys) { | ||||
|             copy[key] = object[key] | ||||
|         } | ||||
|         return <T> copy | ||||
|         return <T>copy | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue