forked from MapComplete/MapComplete
		
	Merge branch 'security/strict-csp' into develop
This commit is contained in:
		
						commit
						394bad545d
					
				
					 54 changed files with 358 additions and 209 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -39,3 +39,4 @@ service-worker.js | |||
| *.vsix | ||||
| public/*.webmanifest | ||||
| public/assets/generated/ | ||||
| public/assets/langs/* | ||||
|  |  | |||
							
								
								
									
										12
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -23,6 +23,7 @@ | |||
|         "chart.js": "^3.8.0", | ||||
|         "country-language": "^0.1.7", | ||||
|         "country-to-currency": "^1.0.10", | ||||
|         "crypto": "^1.0.1", | ||||
|         "csv-parse": "^5.1.0", | ||||
|         "doctest-ts-improved": "^0.8.8", | ||||
|         "dompurify": "^3.0.5", | ||||
|  | @ -5392,6 +5393,12 @@ | |||
|         "node": ">= 8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/crypto": { | ||||
|       "version": "1.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", | ||||
|       "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", | ||||
|       "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." | ||||
|     }, | ||||
|     "node_modules/css-line-break": { | ||||
|       "version": "2.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", | ||||
|  | @ -17341,6 +17348,11 @@ | |||
|         "which": "^2.0.1" | ||||
|       } | ||||
|     }, | ||||
|     "crypto": { | ||||
|       "version": "1.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", | ||||
|       "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" | ||||
|     }, | ||||
|     "css-line-break": { | ||||
|       "version": "2.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", | ||||
|  |  | |||
							
								
								
									
										25
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										25
									
								
								package.json
									
										
									
									
									
								
							|  | @ -17,23 +17,10 @@ | |||
|       "Alternatively, you can override the `osm` credentials using the environment variables `VITE_OSM_OAUTH_CLIENT_ID` and `VITE_OSM_OAUTH_SECRET`" | ||||
|     ], | ||||
|     "oauth_credentials": { | ||||
|       "osm_pietervdvn": { | ||||
|         "#": "This client_id is registered by 'Pieter Vander Vennet' on OSM.org", | ||||
|         "oauth_client_id": "sa1ngLJBJ8McmzHElN8NYtIDm5TZTYEYhq3-0snO4Qc", | ||||
|         "oauth_secret": "XU_cD5Mvw9VKk9T0t_gO8V7cbRC4Hmw2Tb4Rv0Zmz-U", | ||||
|         "url": "https://www.openstreetmap.org" | ||||
|       }, | ||||
|       "osm": { | ||||
|         "#": "This client-id is registered by 'MapComplete' on osm.org", | ||||
|         "oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk", | ||||
|         "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", | ||||
|         "url": "https://www.openstreetmap.org" | ||||
|       }, | ||||
|       "osm-test": { | ||||
|         "oauth_client_id": "HwUn6GPxGm1m9WwMarxTglhy6dBTM4YkaV1I9h6pDGU", | ||||
|         "oauth_secret": "luFZtPJg7j96K6WM6RpcZ_3M-r6muuDq6fG1ygk0I_4", | ||||
|         "url": "https://master.apis.dev.openstreetmap.org" | ||||
|       } | ||||
|       "#": "This client-id is registered by 'MapComplete' on osm.org", | ||||
|       "oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk", | ||||
|       "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", | ||||
|       "url": "https://www.openstreetmap.org" | ||||
|     }, | ||||
|     "api_keys": { | ||||
|       "#": "Various API-keys for various services. Feel free to reuse those in another MapComplete-hosted version", | ||||
|  | @ -45,7 +32,8 @@ | |||
|       "https://overpass.kumi.systems/api/interpreter", | ||||
|       "https://overpass.openstreetmap.ru/cgi/interpreter" | ||||
|     ], | ||||
|     "country_coder_host": "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country" | ||||
|     "country_coder_host": "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country", | ||||
|     "nominatimEndpoint": "https://nominatim.openstreetmap.org/search?" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "start": "npm run generate:layeroverview && npm run strt", | ||||
|  | @ -120,6 +108,7 @@ | |||
|     "chart.js": "^3.8.0", | ||||
|     "country-language": "^0.1.7", | ||||
|     "country-to-currency": "^1.0.10", | ||||
|     "crypto": "^1.0.1", | ||||
|     "csv-parse": "^5.1.0", | ||||
|     "doctest-ts-improved": "^0.8.8", | ||||
|     "dompurify": "^3.0.5", | ||||
|  |  | |||
|  | @ -8,6 +8,8 @@ rm -rf dist/* | |||
| rm -rf .cache | ||||
| mkdir dist 2> /dev/null | ||||
| mkdir dist/assets 2> /dev/null | ||||
| mkdir dist/assets/langs 2> /dev/null | ||||
| mkdir dist/assets/langs/layers 2> /dev/null | ||||
| 
 | ||||
| export NODE_OPTIONS="--max-old-space-size=8192" | ||||
| 
 | ||||
|  | @ -38,7 +40,8 @@ then | |||
|     export ASSET_URL | ||||
|     echo "$ASSET_URL" | ||||
| else | ||||
|   ASSET_URL="$BRANCH" | ||||
|   # ASSET_URL="$BRANCH" | ||||
|   ASSET_URL="./" | ||||
|   export ASSET_URL | ||||
|   echo "$ASSET_URL" | ||||
| fi | ||||
|  | @ -51,5 +54,5 @@ vite build $SRC_MAPS | |||
| cp -r assets/layers/ dist/assets/layers/ | ||||
| cp -r assets/themes/ dist/assets/themes/ | ||||
| cp -r assets/svg/ dist/assets/svg/ | ||||
| 
 | ||||
| cp -r langs/layers/ dist/assets/langs/layers/ | ||||
| export NODE_OPTIONS="" | ||||
|  |  | |||
|  | @ -8,6 +8,11 @@ import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig" | |||
| import xml2js from "xml2js" | ||||
| import ScriptUtils from "./ScriptUtils" | ||||
| import { Utils } from "../src/Utils" | ||||
| import SpecialVisualizations from "../src/UI/SpecialVisualizations" | ||||
| import Constants from "../src/Models/Constants" | ||||
| import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers" | ||||
| import { ImmutableStore } from "../src/Logic/UIEventSource" | ||||
| import * as crypto from "crypto" | ||||
| 
 | ||||
| const sharp = require("sharp") | ||||
| const template = readFileSync("theme.html", "utf8") | ||||
|  | @ -195,31 +200,92 @@ function asLangSpan(t: Translation, tag = "span"): string { | |||
|         if (lang === "_context") { | ||||
|             continue | ||||
|         } | ||||
|         values.push(`<${tag} lang='${lang}'>${t.translations[lang]}</${tag}>`) | ||||
|         values.push(`<${tag} lang="${lang}">${t.translations[lang]}</${tag}>`) | ||||
|     } | ||||
|     return values.join("\n") | ||||
| } | ||||
| 
 | ||||
| let cspCached: string = undefined | ||||
| function generateCsp(): string { | ||||
|     if (cspCached !== undefined) { | ||||
|         return cspCached | ||||
| let previousSrc: Set<string> = new Set<string>() | ||||
| 
 | ||||
| function generateCsp( | ||||
|     layout: LayoutConfig, | ||||
|     options: { | ||||
|         scriptSrcs: string[] | ||||
|     } | ||||
| ): string { | ||||
|     const apiUrls: string[] = [ | ||||
|         "'self'", | ||||
|         ...Constants.defaultOverpassUrls, | ||||
|         Constants.countryCoderEndpoint, | ||||
|         Constants.nominatimEndpoint, | ||||
|         "https://api.openstreetmap.org", | ||||
|         "https://pietervdvn.goatcounter.com", | ||||
|     ].concat(...SpecialVisualizations.specialVisualizations.map((sv) => sv.needsUrls)) | ||||
| 
 | ||||
|     const geojsonSources: string[] = layout.layers.map((l) => l.source?.geojsonSource) | ||||
|     const hosts = new Set<string>() | ||||
|     const eliLayers: RasterLayerPolygon[] = AvailableRasterLayers.layersAvailableAt( | ||||
|         new ImmutableStore({ lon: 0, lat: 0 }) | ||||
|     ).data | ||||
|     const vectorLayers = eliLayers.filter((l) => l.properties.type === "vector") | ||||
|     const vectorSources = vectorLayers.map((l) => l.properties.url) | ||||
|     apiUrls.push(...vectorSources) | ||||
|     for (const connectSource of apiUrls.concat(geojsonSources)) { | ||||
|         if (!connectSource) { | ||||
|             continue | ||||
|         } | ||||
|         try { | ||||
|             const url = new URL(connectSource) | ||||
|             hosts.add("https://" + url.host) | ||||
|         } catch (e) { | ||||
|             hosts.add(connectSource) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     const csp = { | ||||
|     const connectSrc = Array.from(hosts).sort() | ||||
| 
 | ||||
|     const newSrcs = connectSrc.filter((newItem) => !previousSrc.has(newItem)) | ||||
| 
 | ||||
|     console.log( | ||||
|         "Got", | ||||
|         hosts.size, | ||||
|         "connect-src items for theme", | ||||
|         layout.id, | ||||
|         "(extra sources: ", | ||||
|         newSrcs.join(" ") + ")" | ||||
|     ) | ||||
|     previousSrc = hosts | ||||
| 
 | ||||
|     const csp: Record<string, string> = { | ||||
|         "default-src": "'self'", | ||||
|         "script-src": "'self'", | ||||
|         "img-src": "*", | ||||
|         "connect-src": "*", | ||||
|         "script-src": ["'self'", "https://gc.zgo.at/count.js", ...(options?.scriptSrcs ?? [])].join( | ||||
|             " " | ||||
|         ), | ||||
|         "img-src": "* data:", // maplibre depends on 'data:' to load
 | ||||
|         "connect-src": connectSrc.join(" "), | ||||
|         "report-to": "https://report.mapcomplete.org/csp", | ||||
|         "worker-src": "'self' blob:", // Vite somehow loads the worker via a 'blob'
 | ||||
|         "style-src": "'self' 'unsafe-inline'", // unsafe-inline is needed to change the default background pin colours
 | ||||
|     } | ||||
|     const content = Object.keys(csp) | ||||
|         .map((k) => k + ": " + csp[k]) | ||||
|         .map((k) => k + " " + csp[k]) | ||||
|         .join("; ") | ||||
| 
 | ||||
|     cspCached = `<meta http-equiv="Content-Security-Policy" content="${content}">` | ||||
|     return cspCached | ||||
|     return [ | ||||
|         `<meta http-equiv ="Report-To" content='{"group":"csp-endpoint", "max_age": 86400,"endpoints": [\{"url": "https://report.mapcomplete.org/csp"}], "include_subdomains": true}'>`, | ||||
|         `<meta http-equiv="Content-Security-Policy" content="${content}">`, | ||||
|     ].join("\n") | ||||
| } | ||||
| 
 | ||||
| const removeOtherLanguages = readFileSync("./src/UI/RemoveOtherLanguages.js", "utf8") | ||||
|     .split("\n") | ||||
|     .map((s) => s.trim()) | ||||
|     .join("\n") | ||||
| const removeOtherLanguagesHash = crypto | ||||
|     .createHash("sha256") | ||||
|     .update(removeOtherLanguages) | ||||
|     .digest("base64") | ||||
| 
 | ||||
| async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) { | ||||
|     Locale.language.setData(layout.language[0]) | ||||
|     const targetLanguage = layout.language[0] | ||||
|  | @ -290,8 +356,11 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr | |||
|         ...apple_icons, | ||||
|     ].join("\n") | ||||
| 
 | ||||
|     const loadingText = Translations.t.general.loadingTheme.Subs({ theme: ogTitle }) | ||||
| 
 | ||||
|     const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title }) | ||||
|     const templateLines = template.split("\n") | ||||
|     const removeOtherLanguagesReference = templateLines.find( | ||||
|         (line) => line.indexOf("./src/UI/RemoveOtherLanguages.js") >= 0 | ||||
|     ) | ||||
|     let output = template | ||||
|         .replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1")) | ||||
|         .replace( | ||||
|  | @ -299,7 +368,13 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr | |||
|             Translations.t.general.poweredByOsm.textFor(targetLanguage) | ||||
|         ) | ||||
|         .replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific) | ||||
|         .replace(/<!-- CSP -->/, generateCsp()) | ||||
|         .replace( | ||||
|             /<!-- CSP -->/, | ||||
|             generateCsp(layout, { | ||||
|                 scriptSrcs: [`'sha256-${removeOtherLanguagesHash}'`], | ||||
|             }) | ||||
|         ) | ||||
|         .replace(removeOtherLanguagesReference, "<script>" + removeOtherLanguages + "</script>") | ||||
|         .replace( | ||||
|             /<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s, | ||||
|             asLangSpan(layout.shortDescription) | ||||
|  | @ -310,8 +385,8 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr | |||
|         ) | ||||
| 
 | ||||
|         .replace( | ||||
|             '<script src="./src/index.ts" type="module"></script>', | ||||
|             `<script type="module" src='./index_${layout.id}.ts'></script>` | ||||
|             /.*\/src\/index\.ts.*/, | ||||
|             `<script type="module" src="./index_${layout.id}.ts"></script>` | ||||
|         ) | ||||
| 
 | ||||
|     return output | ||||
|  |  | |||
|  | @ -4,13 +4,16 @@ hosted.mapcomplete.org { | |||
| 	header { | ||||
| 		+Permissions-Policy "interest-cohort=()" | ||||
|         +Report-To `\{"group":"csp-endpoint", "max_age": 86400,"endpoints": [\{"url": "https://report.mapcomplete.org/csp"}], "include_subdomains": true}` | ||||
| 		+Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https://gc.zgo.at ; img-src * ; report-uri https://report.mapcomplete.org/csp ; report-to csp-endpoint ;" | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| countrycoder.mapcomplete.org { | ||||
| 	root * tiles/ | ||||
| 	file_server | ||||
| 	header { | ||||
|         +Permissions-Policy "interest-cohort=()" | ||||
|         +Access-Control-Allow-Origin https://hosted.mapcomplete.org https://dev.mapcomplete.org https://mapcomplete.org | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,15 +10,16 @@ | |||
| # unzip tiles.zip | ||||
| 
 | ||||
| MAPCOMPLETE_CONFIGURATION="config_hetzner" | ||||
| cp config.json config.json.bu && | ||||
| cp ./scripts/hetzner/config.json . && # Copy the config _before_ building, as the config might contain some needed URLs | ||||
| npm run reset:layeroverview | ||||
| npm run test | ||||
| cp config.json config.json.bu && | ||||
| cp ./scripts/hetzner/config.json . && | ||||
| npm run prepare-deploy && | ||||
| mv config.json.bu config.json && | ||||
| zip dist.zip -r dist/* && | ||||
| scp -r dist.zip hetzner:/root/ && | ||||
| scp ./scripts/hetzner/config/* hetzner:/root/ | ||||
| ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" | ||||
| scp ./scripts/hetzner/config/* hetzner:/root/ && | ||||
| rsync -rzh --progress dist.zip hetzner:/root/ && | ||||
| echo "Upload completed, deploying config and booting" && | ||||
| ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" && | ||||
| rm dist.zip | ||||
| npm run clean | ||||
|  |  | |||
							
								
								
									
										13
									
								
								src/InstallServiceWorker.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/InstallServiceWorker.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| export {} | ||||
| window.addEventListener("load", async () => { | ||||
|     if (!("serviceWorker" in navigator)) { | ||||
|         console.log("Service workers are not supported") | ||||
|         return | ||||
|     } | ||||
|     try { | ||||
|         await navigator.serviceWorker.register("/service-worker.js") | ||||
|         console.log("Service worker registration successful") | ||||
|     } catch (err) { | ||||
|         console.error("Service worker registration failed", err) | ||||
|     } | ||||
| }) | ||||
|  | @ -23,27 +23,27 @@ export default class AllImageProviders { | |||
|             ) | ||||
|         ), | ||||
|     ] | ||||
| 
 | ||||
|     public static apiUrls: string[] = [].concat( | ||||
|         ...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls()) | ||||
|     ) | ||||
|     public static defaultKeys = [].concat( | ||||
|         AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes) | ||||
|     ) | ||||
|     private static providersByName = { | ||||
|         imgur: Imgur.singleton, | ||||
|         mapillary: Mapillary.singleton, | ||||
|         wikidata: WikidataImageProvider.singleton, | ||||
|         wikimedia: WikimediaImageProvider.singleton, | ||||
|     } | ||||
| 
 | ||||
|     public static byName(name: string) { | ||||
|         return AllImageProviders.providersByName[name.toLowerCase()] | ||||
|     } | ||||
| 
 | ||||
|     public static defaultKeys = [].concat( | ||||
|         AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes) | ||||
|     ) | ||||
| 
 | ||||
|     private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map< | ||||
|         string, | ||||
|         UIEventSource<ProvidedImage[]> | ||||
|     >() | ||||
| 
 | ||||
|     public static byName(name: string) { | ||||
|         return AllImageProviders.providersByName[name.toLowerCase()] | ||||
|     } | ||||
| 
 | ||||
|     public static LoadImagesFor( | ||||
|         tags: Store<Record<string, string>>, | ||||
|         tagKey?: string[] | ||||
|  |  | |||
|  | @ -3,6 +3,10 @@ import ImageProvider, { ProvidedImage } from "./ImageProvider" | |||
| export default class GenericImageProvider extends ImageProvider { | ||||
|     public defaultKeyPrefixes: string[] = ["image"] | ||||
| 
 | ||||
|     public apiUrls(): string[] { | ||||
|         return [] | ||||
|     } | ||||
| 
 | ||||
|     private readonly _valuePrefixBlacklist: string[] | ||||
| 
 | ||||
|     public constructor(valuePrefixBlacklist: string[]) { | ||||
|  |  | |||
|  | @ -65,4 +65,6 @@ export default abstract class ImageProvider { | |||
|     public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> | ||||
| 
 | ||||
|     public abstract DownloadAttribution(url: string): Promise<LicenseInfo> | ||||
| 
 | ||||
|     public abstract apiUrls(): string[] | ||||
| } | ||||
|  |  | |||
|  | @ -10,10 +10,16 @@ export class Imgur extends ImageProvider implements ImageUploader { | |||
|     public static readonly singleton = new Imgur() | ||||
|     public readonly defaultKeyPrefixes: string[] = ["image"] | ||||
|     public readonly maxFileSizeInMegabytes = 10 | ||||
|     public static readonly apiUrl = "https://api.imgur.com/3/image" | ||||
| 
 | ||||
|     private constructor() { | ||||
|         super() | ||||
|     } | ||||
| 
 | ||||
|     apiUrls(): string[] { | ||||
|         return [Imgur.apiUrl] | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Uploads an image, returns the URL where to find the image | ||||
|      * @param title | ||||
|  | @ -25,7 +31,7 @@ export class Imgur extends ImageProvider implements ImageUploader { | |||
|         description: string, | ||||
|         blob: File | ||||
|     ): Promise<{ key: string; value: string }> { | ||||
|         const apiUrl = "https://api.imgur.com/3/image" | ||||
|         const apiUrl = Imgur.apiUrl | ||||
|         const apiKey = Constants.ImgurApiKey | ||||
| 
 | ||||
|         const formData = new FormData() | ||||
|  |  | |||
|  | @ -17,6 +17,10 @@ export class Mapillary extends ImageProvider { | |||
|     ] | ||||
|     defaultKeyPrefixes = ["mapillary", "image"] | ||||
| 
 | ||||
|     apiUrls(): string[] { | ||||
|         return ["https://mapillary.com", "https://www.mapillary.com", "https://graph.mapillary.com"] | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Indicates that this is the same URL | ||||
|      * Ignores 'stp' parameter | ||||
|  |  | |||
|  | @ -5,6 +5,9 @@ import { WikimediaImageProvider } from "./WikimediaImageProvider" | |||
| import Wikidata from "../Web/Wikidata" | ||||
| 
 | ||||
| export class WikidataImageProvider extends ImageProvider { | ||||
|     public apiUrls(): string[] { | ||||
|         return Wikidata.neededUrls | ||||
|     } | ||||
|     public static readonly singleton = new WikidataImageProvider() | ||||
|     public readonly defaultKeyPrefixes = ["wikidata"] | ||||
| 
 | ||||
|  |  | |||
|  | @ -11,11 +11,11 @@ import Wikimedia from "../Web/Wikimedia" | |||
|  */ | ||||
| export class WikimediaImageProvider extends ImageProvider { | ||||
|     public static readonly singleton = new WikimediaImageProvider() | ||||
|     public static readonly commonsPrefixes = [ | ||||
|     public static readonly apiUrls = [ | ||||
|         "https://commons.wikimedia.org/wiki/", | ||||
|         "https://upload.wikimedia.org", | ||||
|         "File:", | ||||
|     ] | ||||
|     public static readonly commonsPrefixes = [...WikimediaImageProvider.apiUrls, "File:"] | ||||
|     private readonly commons_key = "wikimedia_commons" | ||||
|     public readonly defaultKeyPrefixes = [this.commons_key, "image"] | ||||
| 
 | ||||
|  | @ -66,6 +66,10 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|         return value | ||||
|     } | ||||
| 
 | ||||
|     apiUrls(): string[] { | ||||
|         return WikimediaImageProvider.apiUrls | ||||
|     } | ||||
| 
 | ||||
|     SourceIcon(backlink: string): BaseUIElement { | ||||
|         const img = Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em") | ||||
|         if (backlink === undefined) { | ||||
|  |  | |||
|  | @ -32,11 +32,12 @@ export default class Maproulette { | |||
|     private readonly apiKey: string | ||||
| 
 | ||||
|     public static singleton = new Maproulette() | ||||
|     public static readonly defaultEndpoint = "https://maproulette.org/api/v2" | ||||
|     /** | ||||
|      * Creates a new Maproulette instance | ||||
|      * @param endpoint The API endpoint to use | ||||
|      */ | ||||
|     constructor(endpoint: string = "https://maproulette.org/api/v2") { | ||||
|     constructor(endpoint: string = Maproulette.defaultEndpoint) { | ||||
|         this.endpoint = endpoint | ||||
|         this.apiKey = Constants.MaprouletteApiKey | ||||
|     } | ||||
|  |  | |||
							
								
								
									
										6
									
								
								src/Logic/Osm/AuthConfig.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/Logic/Osm/AuthConfig.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| export interface AuthConfig { | ||||
|     "#"?: string // optional comment
 | ||||
|     oauth_client_id: string | ||||
|     oauth_secret: string | ||||
|     url: string | ||||
| } | ||||
|  | @ -1,5 +1,6 @@ | |||
| import { Utils } from "../../Utils" | ||||
| import { BBox } from "../BBox" | ||||
| import Constants from "../../Models/Constants" | ||||
| 
 | ||||
| export interface GeoCodeResult { | ||||
|     display_name: string | ||||
|  | @ -15,7 +16,7 @@ export interface GeoCodeResult { | |||
| } | ||||
| 
 | ||||
| export class Geocoding { | ||||
|     private static readonly host = "https://nominatim.openstreetmap.org/search?" | ||||
|     public static readonly host = Constants.nominatimEndpoint | ||||
| 
 | ||||
|     static async Search(query: string, bbox: BBox): Promise<GeoCodeResult[]> { | ||||
|         const b = bbox ?? BBox.global | ||||
|  |  | |||
|  | @ -4,7 +4,8 @@ import { Store, Stores, UIEventSource } from "../UIEventSource" | |||
| import { OsmPreferences } from "./OsmPreferences" | ||||
| import { Utils } from "../../Utils" | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||
| import * as config from "../../../package.json" | ||||
| import { AuthConfig } from "./AuthConfig" | ||||
| import Constants from "../../Models/Constants" | ||||
| 
 | ||||
| export default class UserDetails { | ||||
|     public loggedIn = false | ||||
|  | @ -25,18 +26,9 @@ export default class UserDetails { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| export interface AuthConfig { | ||||
|     "#"?: string // optional comment
 | ||||
|     oauth_client_id: string | ||||
|     oauth_secret: string | ||||
|     url: string | ||||
| } | ||||
| 
 | ||||
| export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable" | ||||
| 
 | ||||
| export class OsmConnection { | ||||
|     public static readonly oauth_configs: Record<string, AuthConfig> = | ||||
|         config.config.oauth_credentials | ||||
|     public auth | ||||
|     public userDetails: UIEventSource<UserDetails> | ||||
|     public isLoggedIn: Store<boolean> | ||||
|  | @ -53,7 +45,7 @@ export class OsmConnection { | |||
|     public preferencesHandler: OsmPreferences | ||||
|     public readonly _oauth_config: AuthConfig | ||||
|     private readonly _dryRun: Store<boolean> | ||||
|     private fakeUser: boolean | ||||
|     private readonly fakeUser: boolean | ||||
|     private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [] | ||||
|     private readonly _iframeMode: Boolean | boolean | ||||
|     private readonly _singlePage: boolean | ||||
|  | @ -65,15 +57,12 @@ export class OsmConnection { | |||
|         oauth_token?: UIEventSource<string> | ||||
|         // Used to keep multiple changesets open and to write to the correct changeset
 | ||||
|         singlePage?: boolean | ||||
|         osmConfiguration?: "osm" | "osm-test" | ||||
|         attemptLogin?: true | boolean | ||||
|     }) { | ||||
|         options = options ?? {} | ||||
|         this.fakeUser = options.fakeUser ?? false | ||||
|         this._singlePage = options.singlePage ?? true | ||||
|         this._oauth_config = | ||||
|             OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ?? | ||||
|             OsmConnection.oauth_configs.osm | ||||
|         options ??= {} | ||||
|         this.fakeUser = options?.fakeUser ?? false | ||||
|         this._singlePage = options?.singlePage ?? true | ||||
|         this._oauth_config = Constants.osmAuthConfig | ||||
|         console.debug("Using backend", this._oauth_config.url) | ||||
|         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,14 +28,8 @@ class FeatureSwitchUtils { | |||
| 
 | ||||
| export class OsmConnectionFeatureSwitches { | ||||
|     public readonly featureSwitchFakeUser: UIEventSource<boolean> | ||||
|     public readonly featureSwitchApiURL: UIEventSource<string> | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.featureSwitchApiURL = QueryParameters.GetQueryParameter( | ||||
|             "backend", | ||||
|             "osm", | ||||
|             "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'" | ||||
|         ) | ||||
| 
 | ||||
|         this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter( | ||||
|             "fake-user", | ||||
|  | @ -143,7 +137,6 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches { | |||
| 
 | ||||
|         let testingDefaultValue = false | ||||
|         if ( | ||||
|             this.featureSwitchApiURL.data !== "osm-test" && | ||||
|             !Utils.runningFromConsole && | ||||
|             (location.hostname === "localhost" || location.hostname === "127.0.0.1") | ||||
|         ) { | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import { Mapillary } from "../ImageProviders/Mapillary" | ||||
| import P4C from "pic4carto" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| export interface NearbyImageOptions { | ||||
|     lon: number | ||||
|     lat: number | ||||
|  | @ -35,17 +35,12 @@ export interface P4CPicture { | |||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Uses Pic4wCarto to fetch nearby images from various providers | ||||
|  * Uses Pic4Carto to fetch nearby images from various providers | ||||
|  */ | ||||
| export default class NearbyImagesSearch { | ||||
|     private static readonly services = [ | ||||
|         "mapillary", | ||||
|         "flickr", | ||||
|         "openstreetcam", | ||||
|         "wikicommons", | ||||
|     ] as const | ||||
| 
 | ||||
|     private individualStores | ||||
|     public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const | ||||
|     public static readonly apiUrls = ["https://api.flickr.com"] | ||||
|     private readonly individualStores: Store<{ images: P4CPicture[]; beforeFilter: number }>[] | ||||
|     private readonly _store: UIEventSource<P4CPicture[]> = new UIEventSource<P4CPicture[]>([]) | ||||
|     public readonly store: Store<P4CPicture[]> = this._store | ||||
|     private readonly _options: NearbyImageOptions | ||||
|  | @ -71,16 +66,16 @@ export default class NearbyImagesSearch { | |||
|         this.update() | ||||
|     } | ||||
| 
 | ||||
|     private static buildPictureFetcher( | ||||
|     private static async fetchImages( | ||||
|         options: NearbyImageOptions, | ||||
|         fetcher: "mapillary" | "flickr" | "openstreetcam" | "wikicommons" | ||||
|     ): Store<{ images: P4CPicture[]; beforeFilter: number }> { | ||||
|         fetcher: P4CService | ||||
|     ): Promise<P4CPicture[]> { | ||||
|         const picManager = new P4C.PicturesManager({ usefetchers: [fetcher] }) | ||||
|         const searchRadius = options.searchRadius ?? 100 | ||||
|         const maxAgeSeconds = (options.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000 | ||||
|         const searchRadius = options.searchRadius ?? 100 | ||||
| 
 | ||||
|         const p4cStore = Stores.FromPromise<P4CPicture[]>( | ||||
|             picManager.startPicsRetrievalAround( | ||||
|         try { | ||||
|             const pics: P4CPicture[] = await picManager.startPicsRetrievalAround( | ||||
|                 new P4C.LatLng(options.lat, options.lon), | ||||
|                 searchRadius, | ||||
|                 { | ||||
|  | @ -88,7 +83,21 @@ export default class NearbyImagesSearch { | |||
|                     towardscenter: false, | ||||
|                 } | ||||
|             ) | ||||
|             return pics | ||||
|         } catch (e) { | ||||
|             console.error("Could not fetch images from service", fetcher, e) | ||||
|             return [] | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static buildPictureFetcher( | ||||
|         options: NearbyImageOptions, | ||||
|         fetcher: P4CService | ||||
|     ): Store<{ images: P4CPicture[]; beforeFilter: number }> { | ||||
|         const p4cStore = Stores.FromPromise<P4CPicture[]>( | ||||
|             NearbyImagesSearch.fetchImages(options, fetcher) | ||||
|         ) | ||||
|         const searchRadius = options.searchRadius ?? 100 | ||||
|         return p4cStore.map( | ||||
|             (images) => { | ||||
|                 if (images === undefined) { | ||||
|  | @ -220,3 +229,5 @@ class ImagesInLoadedDataFetcher { | |||
|         return foundImages | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| type P4CService = (typeof NearbyImagesSearch.services)[number] | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| export default class PlantNet { | ||||
|     private static baseUrl = | ||||
|     public static baseUrl = | ||||
|         "https://my-api.plantnet.org/v2/identify/all?api-key=2b10AAsjzwzJvucA5Ncm5qxe" | ||||
| 
 | ||||
|     public static query(imageUrls: string[]): Promise<PlantNetResult> { | ||||
|  |  | |||
|  | @ -123,6 +123,11 @@ export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions { | |||
|  * Utility functions around wikidata | ||||
|  */ | ||||
| export default class Wikidata { | ||||
|     public static readonly neededUrls = [ | ||||
|         "https://www.wikidata.org/", | ||||
|         "https://wikidata.org/", | ||||
|         "https://query.wikidata.org", | ||||
|     ] | ||||
|     private static readonly _identifierPrefixes = ["Q", "L"].map((str) => str.toLowerCase()) | ||||
|     private static readonly _prefixesToRemove = [ | ||||
|         "https://www.wikidata.org/wiki/Lexeme:", | ||||
|  | @ -130,11 +135,11 @@ export default class Wikidata { | |||
|         "http://www.wikidata.org/entity/", | ||||
|         "Lexeme:", | ||||
|     ].map((str) => str.toLowerCase()) | ||||
| 
 | ||||
|     private static readonly _storeCache = new Map< | ||||
|         string, | ||||
|         Store<{ success: WikidataResponse } | { error: any }> | ||||
|     >() | ||||
| 
 | ||||
|     /** | ||||
|      * Same as LoadWikidataEntry, but wrapped into a UIEventSource | ||||
|      * @param value | ||||
|  | @ -388,6 +393,7 @@ export default class Wikidata { | |||
|     } | ||||
| 
 | ||||
|     private static _cache = new Map<string, Promise<WikidataResponse>>() | ||||
| 
 | ||||
|     public static async LoadWikidataEntryAsync(value: string | number): Promise<WikidataResponse> { | ||||
|         const key = "" + value | ||||
|         const cached = Wikidata._cache.get(key) | ||||
|  | @ -398,6 +404,7 @@ export default class Wikidata { | |||
|         Wikidata._cache.set(key, uncached) | ||||
|         return uncached | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Loads a wikidata page | ||||
|      * @returns the entity of the given value | ||||
|  |  | |||
|  | @ -34,6 +34,8 @@ export default class Wikipedia { | |||
| 
 | ||||
|     private static readonly idsToRemove = ["sjabloon_zie"] | ||||
| 
 | ||||
|     public static readonly neededUrls = ["*.wikipedia.org"] | ||||
| 
 | ||||
|     private static readonly _cache = new Map<string, Promise<string>>() | ||||
|     private static _fullDetailsCache = new Map<string, Store<FullWikipediaDetails>>() | ||||
|     public readonly backend: string | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import * as packagefile from "../../package.json" | ||||
| import * as extraconfig from "../../config.json" | ||||
| import { Utils } from "../Utils" | ||||
| import { AuthConfig } from "../Logic/Osm/AuthConfig" | ||||
| 
 | ||||
| export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number] | ||||
| 
 | ||||
|  | @ -104,8 +105,9 @@ export default class Constants { | |||
|     public static ImgurApiKey = Constants.config.api_keys.imgur | ||||
|     public static readonly mapillary_client_token_v4 = Constants.config.api_keys.mapillary_v4 | ||||
|     public static defaultOverpassUrls = Constants.config.default_overpass_urls | ||||
|     static countryCoderEndpoint: string = Constants.config.country_coder_host | ||||
| 
 | ||||
|     public static countryCoderEndpoint: string = Constants.config.country_coder_host | ||||
|     public static osmAuthConfig: AuthConfig = Constants.config.oauth_credentials | ||||
|     public static nominatimEndpoint: string = Constants.config.nominatimEndpoint | ||||
|     /** | ||||
|      * These are the values that are allowed to use as 'backdrop' icon for a map pin | ||||
|      */ | ||||
|  |  | |||
|  | @ -140,8 +140,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|                 "oauth_token", | ||||
|                 undefined, | ||||
|                 "Used to complete the login" | ||||
|             ), | ||||
|             osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data, | ||||
|             ) | ||||
|         }) | ||||
|         this.userRelatedState = new UserRelatedState( | ||||
|             this.osmConnection, | ||||
|  |  | |||
|  | @ -22,8 +22,7 @@ export default class AllThemesGui { | |||
|                     "oauth_token", | ||||
|                     undefined, | ||||
|                     "Used to complete the login" | ||||
|                 ), | ||||
|                 osmConfiguration: <"osm" | "osm-test">featureSwitches.featureSwitchApiURL.data, | ||||
|                 ) | ||||
|             }) | ||||
|             const state = new UserRelatedState(osmConnection) | ||||
|             const intro = new Combine([ | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import { Utils } from "../../Utils" | |||
| import Constants from "../../Models/Constants" | ||||
| 
 | ||||
| export class OpenJosm extends Combine { | ||||
|     public static readonly needsUrls = ["http://127.0.0.1:8111/load_and_zoom"] | ||||
|     constructor(osmConnection: OsmConnection, bounds: Store<BBox>, iconStyle?: string) { | ||||
|         const t = Translations.t.general.attribution | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,9 +10,11 @@ import Combine from "../Base/Combine" | |||
| import Title from "../Base/Title" | ||||
| import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| import Constants from "../../Models/Constants" | ||||
| 
 | ||||
| export class AddNoteCommentViz implements SpecialVisualization { | ||||
|     funcName = "add_note_comment" | ||||
|     needsUrls = [Constants.osmAuthConfig.url] | ||||
|     docs = "A textfield to add a comment to a node (with the option to close the note)." | ||||
|     args = [ | ||||
|         { | ||||
|  |  | |||
|  | @ -9,7 +9,6 @@ import { Utils } from "../../Utils" | |||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" | ||||
| import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| import Loading from "../Base/Loading" | ||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||
| import Translations from "../i18n/Translations" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import { Changes } from "../../Logic/Osm/Changes" | ||||
|  | @ -209,6 +208,8 @@ class ApplyButton extends UIElement { | |||
| export default class AutoApplyButton implements SpecialVisualization { | ||||
|     public readonly docs: BaseUIElement | ||||
|     public readonly funcName: string = "auto_apply" | ||||
|     public readonly needsUrls = [] | ||||
| 
 | ||||
|     public readonly args: { | ||||
|         name: string | ||||
|         defaultValue?: string | ||||
|  | @ -271,14 +272,7 @@ export default class AutoApplyButton implements SpecialVisualization { | |||
|         argument: string[] | ||||
|     ): BaseUIElement { | ||||
|         try { | ||||
|             if ( | ||||
|                 !state.layout.official && | ||||
|                 !( | ||||
|                     state.featureSwitchIsTesting.data || | ||||
|                     state.osmConnection._oauth_config.url === | ||||
|                         OsmConnection.oauth_configs["osm-test"].url | ||||
|                 ) | ||||
|             ) { | ||||
|             if (!state.layout.official && !state.featureSwitchIsTesting.data) { | ||||
|                 const t = Translations.t.general.add.import | ||||
|                 return new Combine([ | ||||
|                     new FixedUiElement( | ||||
|  |  | |||
|  | @ -8,9 +8,11 @@ import Toggle from "../Input/Toggle" | |||
| import { LoginToggle } from "./LoginButton" | ||||
| import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| import Constants from "../../Models/Constants" | ||||
| 
 | ||||
| export class CloseNoteButton implements SpecialVisualization { | ||||
|     public readonly funcName = "close_note" | ||||
|     public readonly needsUrls = [Constants.osmAuthConfig.url] | ||||
|     public readonly docs = | ||||
|         "Button to close a note. A predifined text can be defined to close the note with. If the note is already closed, will show a small text." | ||||
|     public readonly args = [ | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ export class ExportAsGpxViz implements SpecialVisualization { | |||
|     funcName = "export_as_gpx" | ||||
|     docs = "Exports the selected feature as GPX-file" | ||||
|     args = [] | ||||
| 
 | ||||
|     needsUrls = [] | ||||
|     constr( | ||||
|         state: SpecialVisualizationState, | ||||
|         tagSource: UIEventSource<Record<string, string>>, | ||||
|  |  | |||
|  | @ -2,10 +2,13 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" | |||
| import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||
| import Histogram from "../BigComponents/Histogram" | ||||
| import { Feature } from "geojson" | ||||
| import Constants from "../../Models/Constants" | ||||
| 
 | ||||
| export class HistogramViz implements SpecialVisualization { | ||||
|     funcName = "histogram" | ||||
|     docs = "Create a histogram for a list of given values, read from the properties." | ||||
|     needsUrls = [] | ||||
| 
 | ||||
|     example = | ||||
|         '`{histogram(\'some_key\')}` with properties being `{some_key: ["a","b","a","c"]} to create a histogram' | ||||
|     args = [ | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ export interface ConflateFlowArguments extends ImportFlowArguments { | |||
| 
 | ||||
| export default class ConflateImportButtonViz implements SpecialVisualization, AutoAction { | ||||
|     supportsAutoAction: boolean = true | ||||
|     needsUrls = [] | ||||
|     public readonly funcName: string = "conflate_button" | ||||
|     public readonly args: { | ||||
|         name: string | ||||
|  |  | |||
|  | @ -194,10 +194,7 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> { | |||
|                     return { error: t.hasBeenImported } | ||||
|                 } | ||||
| 
 | ||||
|                 const usesTestUrl = | ||||
|                     this.state.osmConnection._oauth_config.url === | ||||
|                     OsmConnection.oauth_configs["osm-test"].url | ||||
|                 if (!state.layout.official && !(isTesting || usesTestUrl)) { | ||||
|                 if (!state.layout.official && !isTesting) { | ||||
|                     // Unofficial theme - imports not allowed
 | ||||
|                     return { | ||||
|                         error: t.officialThemesOnly, | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ export class PointImportButtonViz implements SpecialVisualization { | |||
|     public readonly docs: string | BaseUIElement | ||||
|     public readonly example?: string | ||||
|     public readonly args: { name: string; defaultValue?: string; doc: string }[] | ||||
|     public needsUrls = [] | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.funcName = "import_button" | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSou | |||
|  */ | ||||
| export default class WayImportButtonViz implements AutoAction, SpecialVisualization { | ||||
|     public readonly funcName: string = "import_way_button" | ||||
|     needsUrls = [] | ||||
|     public readonly docs: string = | ||||
|         "This button will copy the data from an external dataset into OpenStreetMap, copying the geometry and adding it as a 'line'" + | ||||
|         ImportFlowUtils.documentationGeneral | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ import { Feature } from "geojson" | |||
| 
 | ||||
| export class LanguageElement implements SpecialVisualization { | ||||
|     funcName: string = "language_chooser" | ||||
|     needsUrls = [] | ||||
| 
 | ||||
|     docs: string | BaseUIElement = | ||||
|         "The language element allows to show and pick all known (modern) languages. The key can be set" | ||||
|  |  | |||
|  | @ -9,6 +9,8 @@ import MapillaryLink from "../BigComponents/MapillaryLink.svelte" | |||
| export class MapillaryLinkVis implements SpecialVisualization { | ||||
|     funcName = "mapillary_link" | ||||
|     docs = "Adds a button to open mapillary on the specified location" | ||||
|     needsUrls = [] | ||||
| 
 | ||||
|     args = [ | ||||
|         { | ||||
|             name: "zoom", | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import { BBox } from "../../Logic/BBox" | |||
| export class MinimapViz implements SpecialVisualization { | ||||
|     funcName = "minimap" | ||||
|     docs = "A small map showing the selected feature." | ||||
|     needsUrls = [] | ||||
|     args = [ | ||||
|         { | ||||
|             doc: "The (maximum) zoomlevel: the target zoomlevel after fitting the entire feature. The minimap will fit the entire feature, then zoom out to this zoom level. The higher, the more zoomed in with 1 being the entire world and 19 being really close", | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisua | |||
| 
 | ||||
| export class MultiApplyViz implements SpecialVisualization { | ||||
|     funcName = "multi_apply" | ||||
|     needsUrls = [] | ||||
|     docs = | ||||
|         "A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags" | ||||
|     args = [ | ||||
|  |  | |||
|  | @ -8,9 +8,10 @@ import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" | |||
| import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||
| import SvelteUIElement from "../Base/SvelteUIElement" | ||||
| import PlantNet from "../PlantNet/PlantNet.svelte" | ||||
| 
 | ||||
| import { default as PlantNetCode } from "../../Logic/Web/PlantNet" | ||||
| export class PlantNetDetectionViz implements SpecialVisualization { | ||||
|     funcName = "plantnet_detection" | ||||
|     needsUrls = [PlantNetCode.baseUrl] | ||||
| 
 | ||||
|     docs = | ||||
|         "Sends the images linked to the current object to plantnet.org and asks it what plant species is shown on it. The user can then select the correct species; the corresponding wikidata-identifier will then be added to the object (together with `source:species:wikidata=plantnet.org AI`). " | ||||
|  |  | |||
|  | @ -11,6 +11,8 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | |||
|  */ | ||||
| export default class QuestionViz implements SpecialVisualization { | ||||
|     funcName = "questions" | ||||
|     needsUrls = [] | ||||
| 
 | ||||
|     docs = | ||||
|         "The special element which shows the questions which are unkown. Added by default if not yet there" | ||||
|     args = [ | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ export class ShareLinkViz implements SpecialVisualization { | |||
|             doc: "The url to share (default: current URL)", | ||||
|         }, | ||||
|     ] | ||||
|     needsUrls = [] | ||||
| 
 | ||||
|     public constr( | ||||
|         state: SpecialVisualizationState, | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ import Maproulette from "../../Logic/Maproulette" | |||
| 
 | ||||
| export default class TagApplyButton implements AutoAction, SpecialVisualization { | ||||
|     public readonly funcName = "tag_apply" | ||||
|     needsUrls = [] | ||||
|     public readonly docs = | ||||
|         "Shows a big button; clicking this button will apply certain tags onto the feature.\n\nThe first argument takes a specification of which tags to add.\n" + | ||||
|         Utils.Special_visualizations_tagsToApplyHelpText | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI" | |||
| import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { GeoOperations } from "../../Logic/GeoOperations" | ||||
| import Constants from "../../Models/Constants" | ||||
| 
 | ||||
| /** | ||||
|  * Wrapper  around 'UploadTraceToOsmUI' | ||||
|  | @ -11,6 +12,7 @@ export class UploadToOsmViz implements SpecialVisualization { | |||
|     docs = | ||||
|         "Uploads the GPS-history as GPX to OpenStreetMap.org; clears the history afterwards. The actual feature is ignored." | ||||
|     args = [] | ||||
|     needsUrls = [Constants.osmAuthConfig.url] | ||||
| 
 | ||||
|     constr( | ||||
|         state: SpecialVisualizationState, | ||||
|  |  | |||
							
								
								
									
										31
									
								
								src/UI/RemoveOtherLanguages.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/UI/RemoveOtherLanguages.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| let lang = ( | ||||
|   (navigator.languages && navigator.languages[0]) || | ||||
|   navigator.language || | ||||
|   navigator["userLanguage"] || | ||||
|   "en" | ||||
| ).substr(0, 2) | ||||
| 
 | ||||
| function filterLangs(maindiv) { | ||||
|   let foundLangs = 0 | ||||
|   for (const child of Array.from(maindiv.children)) { | ||||
|     if (child.attributes.getNamedItem("lang")?.value === lang) { | ||||
|       foundLangs++ | ||||
|     } | ||||
|   } | ||||
|   if (foundLangs === 0) { | ||||
|     lang = "en" | ||||
|   } | ||||
|   for (const child of Array.from(maindiv.children)) { | ||||
|     const childLang = child.attributes.getNamedItem("lang") | ||||
|     if (childLang === undefined) { | ||||
|       continue | ||||
|     } | ||||
|     if (childLang.value === lang) { | ||||
|       continue | ||||
|     } | ||||
|     child.parentElement.removeChild(child) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| filterLangs(document.getElementById("descriptions-while-loading")) | ||||
| filterLangs(document.getElementById("default-title")) | ||||
|  | @ -88,6 +88,7 @@ export interface SpecialVisualization { | |||
|     readonly funcName: string | ||||
|     readonly docs: string | BaseUIElement | ||||
|     readonly example?: string | ||||
|     readonly needsUrls: string[] | ||||
| 
 | ||||
|     /** | ||||
|      * Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included | ||||
|  |  | |||
|  | @ -28,7 +28,6 @@ import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata" | |||
| import { Translation } from "./i18n/Translation" | ||||
| import Translations from "./i18n/Translations" | ||||
| import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization" | ||||
| import LiveQueryHandler from "../Logic/Web/LiveQueryHandler" | ||||
| import { SubtleButton } from "./Base/SubtleButton" | ||||
| import Svg from "../Svg" | ||||
| import NoteCommentElement from "./Popup/NoteCommentElement" | ||||
|  | @ -68,6 +67,11 @@ import SendEmail from "./Popup/SendEmail.svelte" | |||
| import NearbyImages from "./Popup/NearbyImages.svelte" | ||||
| import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte" | ||||
| import UploadImage from "./Image/UploadImage.svelte" | ||||
| import { Imgur } from "../Logic/ImageProviders/Imgur" | ||||
| import Constants from "../Models/Constants" | ||||
| import { MangroveReviews } from "mangrove-reviews-typescript" | ||||
| import Wikipedia from "../Logic/Web/Wikipedia" | ||||
| import NearbyImagesSearch from "../Logic/Web/NearbyImagesSearch" | ||||
| import AllReviews from "./Reviews/AllReviews.svelte" | ||||
| import StarsBarIcon from "./Reviews/StarsBarIcon.svelte" | ||||
| import ReviewForm from "./Reviews/ReviewForm.svelte" | ||||
|  | @ -84,7 +88,7 @@ class NearbyImageVis implements SpecialVisualization { | |||
|     docs = | ||||
|         "A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature" | ||||
|     funcName = "nearby_images" | ||||
| 
 | ||||
|     needsUrls = NearbyImagesSearch.apiUrls | ||||
|     constr( | ||||
|         state: SpecialVisualizationState, | ||||
|         tags: UIEventSource<Record<string, string>>, | ||||
|  | @ -122,6 +126,7 @@ class StealViz implements SpecialVisualization { | |||
|             required: true, | ||||
|         }, | ||||
|     ] | ||||
|     needsUrls = [] | ||||
| 
 | ||||
|     constr(state: SpecialVisualizationState, featureTags, args) { | ||||
|         const [featureIdKey, layerAndtagRenderingIds] = args | ||||
|  | @ -382,6 +387,7 @@ export default class SpecialVisualizations { | |||
|                 funcName: "add_new_point", | ||||
|                 docs: "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 constr(state: SpecialVisualizationState, _, __, feature): BaseUIElement { | ||||
|                     let [lon, lat] = GeoOperations.centerpointCoordinates(feature) | ||||
|                     return new SvelteUIElement(AddNewPoint, { | ||||
|  | @ -393,6 +399,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "user_profile", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 docs: "A component showing information about the currently logged in user (username, profile description, profile picture + link to edit them). Mostly meant to be used in the 'user-settings'", | ||||
|                 constr(state: SpecialVisualizationState): BaseUIElement { | ||||
|                     return new SvelteUIElement(UserProfile, { | ||||
|  | @ -403,6 +410,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "language_picker", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 docs: "A component to set the language of the user interface", | ||||
|                 constr(state: SpecialVisualizationState): BaseUIElement { | ||||
|                     return new LanguagePicker( | ||||
|  | @ -414,6 +422,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "logout", | ||||
|                 args: [], | ||||
|                 needsUrls: [Constants.osmAuthConfig.url], | ||||
|                 docs: "Shows a button where the user can log out", | ||||
|                 constr(state: SpecialVisualizationState): BaseUIElement { | ||||
|                     return new SubtleButton(Svg.logout_svg(), Translations.t.general.logout, { | ||||
|  | @ -430,6 +439,7 @@ export default class SpecialVisualizations { | |||
|                 funcName: "split_button", | ||||
|                 docs: "Adds a button which allows to split a way", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 constr( | ||||
|                     state: SpecialVisualizationState, | ||||
|                     tagSource: UIEventSource<Record<string, string>> | ||||
|  | @ -450,6 +460,7 @@ export default class SpecialVisualizations { | |||
|                 funcName: "move_button", | ||||
|                 docs: "Adds a button which allows to move the object to another location. The config will be read from the layer config", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 constr( | ||||
|                     state: SpecialVisualizationState, | ||||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|  | @ -473,6 +484,7 @@ export default class SpecialVisualizations { | |||
|                 funcName: "delete_button", | ||||
|                 docs: "Adds a button which allows to delete the object at this location. The config will be read from the layer config", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 constr( | ||||
|                     state: SpecialVisualizationState, | ||||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|  | @ -497,6 +509,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "open_note", | ||||
|                 args: [], | ||||
|                 needsUrls: [Constants.osmAuthConfig.url], | ||||
|                 docs: "Creates a new map note on the given location. This options is placed in the 'last_click'-popup automatically if the 'notes'-layer is enabled", | ||||
|                 constr( | ||||
|                     state: SpecialVisualizationState, | ||||
|  | @ -529,6 +542,7 @@ export default class SpecialVisualizations { | |||
|                         defaultValue: "wikidata;wikipedia", | ||||
|                     }, | ||||
|                 ], | ||||
|                 needsUrls: [...Wikidata.neededUrls, ...Wikipedia.neededUrls], | ||||
|                 example: | ||||
|                     "`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height", | ||||
|                 constr: (_, tagsSource, args) => { | ||||
|  | @ -552,6 +566,7 @@ export default class SpecialVisualizations { | |||
|                         defaultValue: "wikidata", | ||||
|                     }, | ||||
|                 ], | ||||
|                 needsUrls: Wikidata.neededUrls, | ||||
|                 example: | ||||
|                     "`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself", | ||||
|                 constr: (_, tagsSource, args) => | ||||
|  | @ -581,6 +596,7 @@ export default class SpecialVisualizations { | |||
|                 funcName: "all_tags", | ||||
|                 docs: "Prints all key-value pairs of the object - used for debugging", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 constr: (state, tags: UIEventSource<any>) => | ||||
|                     new SvelteUIElement(AllTagsPanel, { tags, state }), | ||||
|             }, | ||||
|  | @ -594,6 +610,7 @@ export default class SpecialVisualizations { | |||
|                         doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ", | ||||
|                     }, | ||||
|                 ], | ||||
|                 needsUrls: AllImageProviders.apiUrls, | ||||
|                 constr: (state, tags, args) => { | ||||
|                     let imagePrefixes: string[] = undefined | ||||
|                     if (args.length > 0) { | ||||
|  | @ -609,6 +626,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "image_upload", | ||||
|                 docs: "Creates a button where a user can upload an image to IMGUR", | ||||
|                 needsUrls: [Imgur.apiUrl], | ||||
|                 args: [ | ||||
|                     { | ||||
|                         name: "image-key", | ||||
|  | @ -633,6 +651,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "rating", | ||||
|                 docs: "Shows stars which represent the avarage rating on mangrove.reviews", | ||||
|                 needsUrls: [MangroveReviews.ORIGINAL_API], | ||||
|                 args: [ | ||||
|                     { | ||||
|                         name: "subjectKey", | ||||
|  | @ -670,6 +689,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "create_review", | ||||
|                 docs: "Invites the contributor to leave a review. Somewhat small UI-element until interacted", | ||||
|                 needsUrls: [MangroveReviews.ORIGINAL_API], | ||||
|                 args: [ | ||||
|                     { | ||||
|                         name: "subjectKey", | ||||
|  | @ -699,6 +719,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "list_reviews", | ||||
|                 docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten", | ||||
|                 needsUrls: [MangroveReviews.ORIGINAL_API], | ||||
|                 example: | ||||
|                     "`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used", | ||||
|                 args: [ | ||||
|  | @ -747,6 +768,7 @@ export default class SpecialVisualizations { | |||
|                         doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__", | ||||
|                     }, | ||||
|                 ], | ||||
|                 needsUrls: [], | ||||
|                 example: | ||||
|                     "A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`", | ||||
|                 constr: (state, tagSource: UIEventSource<any>, args) => { | ||||
|  | @ -759,38 +781,9 @@ export default class SpecialVisualizations { | |||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|             { | ||||
|                 funcName: "live", | ||||
|                 docs: "Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}", | ||||
|                 example: | ||||
|                     "{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}", | ||||
|                 args: [ | ||||
|                     { | ||||
|                         name: "Url", | ||||
|                         doc: "The URL to load", | ||||
|                         required: true, | ||||
|                     }, | ||||
|                     { | ||||
|                         name: "Shorthands", | ||||
|                         doc: "A list of shorthands, of the format 'shorthandname:path.path.path'. separated by ;", | ||||
|                     }, | ||||
|                     { | ||||
|                         name: "path", | ||||
|                         doc: "The path (or shorthand) that should be returned", | ||||
|                     }, | ||||
|                 ], | ||||
|                 constr: (_, tagSource: UIEventSource<any>, args) => { | ||||
|                     const url = args[0] | ||||
|                     const shorthands = args[1] | ||||
|                     const neededValue = args[2] | ||||
|                     const source = LiveQueryHandler.FetchLiveData(url, shorthands.split(";")) | ||||
|                     return new VariableUiElement( | ||||
|                         source.map((data) => data[neededValue] ?? "Loading...") | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|             { | ||||
|                 funcName: "canonical", | ||||
|                 needsUrls: [], | ||||
|                 docs: "Converts a short, canonical value into the long, translated text including the unit. This only works if a `unit` is defined for the corresponding value. The unit specification will be included in the text. ", | ||||
|                 example: | ||||
|                     "If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ...", | ||||
|  | @ -828,6 +821,7 @@ export default class SpecialVisualizations { | |||
|                 funcName: "export_as_geojson", | ||||
|                 docs: "Exports the selected feature as GeoJson-file", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 constr: (state, tagSource, tagsSource, feature, layer) => { | ||||
|                     const t = Translations.t.general.download | ||||
| 
 | ||||
|  | @ -857,6 +851,7 @@ export default class SpecialVisualizations { | |||
|                 funcName: "open_in_iD", | ||||
|                 docs: "Opens the current view in the iD-editor", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 constr: (state, feature) => { | ||||
|                     return new SvelteUIElement(OpenIdEditor, { | ||||
|                         mapProperties: state.mapProperties, | ||||
|  | @ -868,6 +863,8 @@ export default class SpecialVisualizations { | |||
|                 funcName: "open_in_josm", | ||||
|                 docs: "Opens the current view in the JOSM-editor", | ||||
|                 args: [], | ||||
|                 needsUrls: OpenJosm.needsUrls, | ||||
| 
 | ||||
|                 constr: (state) => { | ||||
|                     return new OpenJosm(state.osmConnection, state.mapProperties.bounds) | ||||
|                 }, | ||||
|  | @ -876,6 +873,7 @@ export default class SpecialVisualizations { | |||
|                 funcName: "clear_location_history", | ||||
|                 docs: "A button to remove the travelled track information from the device", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 constr: (state) => { | ||||
|                     return new SubtleButton( | ||||
|                         Svg.delete_icon_svg().SetStyle("height: 1.5rem"), | ||||
|  | @ -901,6 +899,7 @@ export default class SpecialVisualizations { | |||
|                         defaultValue: "0", | ||||
|                     }, | ||||
|                 ], | ||||
|                 needsUrls: [Constants.osmAuthConfig.url], | ||||
|                 constr: (state, tags, args) => | ||||
|                     new VariableUiElement( | ||||
|                         tags | ||||
|  | @ -929,6 +928,7 @@ export default class SpecialVisualizations { | |||
|                         defaultValue: "id", | ||||
|                     }, | ||||
|                 ], | ||||
|                 needsUrls: [Imgur.apiUrl], | ||||
|                 constr: (state, tags, args) => { | ||||
|                     const id = tags.data[args[0] ?? "id"] | ||||
|                     tags = state.featureProperties.getStore(id) | ||||
|  | @ -939,6 +939,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "title", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'", | ||||
|                 example: | ||||
|                     "`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.", | ||||
|  | @ -959,6 +960,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "maproulette_task", | ||||
|                 args: [], | ||||
|                 needsUrls: [Maproulette.defaultEndpoint], | ||||
|                 constr(state, tagSource) { | ||||
|                     let parentId = tagSource.data.mr_challengeId | ||||
|                     if (parentId === undefined) { | ||||
|  | @ -1002,6 +1004,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "maproulette_set_status", | ||||
|                 docs: "Change the status of the given MapRoulette task", | ||||
|                 needsUrls: [Maproulette.defaultEndpoint], | ||||
|                 example: | ||||
|                     " The following example sets the status to '2' (false positive)\n" + | ||||
|                     "\n" + | ||||
|  | @ -1125,6 +1128,7 @@ export default class SpecialVisualizations { | |||
|                 funcName: "statistics", | ||||
|                 docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 constr: (state) => { | ||||
|                     return new Combine( | ||||
|                         state.layout.layers | ||||
|  | @ -1167,6 +1171,8 @@ export default class SpecialVisualizations { | |||
|                         required: true, | ||||
|                     }, | ||||
|                 ], | ||||
|                 needsUrls: [], | ||||
| 
 | ||||
|                 constr(__, tags, args) { | ||||
|                     return new SvelteUIElement(SendEmail, { args, tags }) | ||||
|                 }, | ||||
|  | @ -1194,6 +1200,7 @@ export default class SpecialVisualizations { | |||
|                         doc: "If set, this link will act as a download-button. The contents of `href` will be offered for download; this parameter will act as the proposed filename", | ||||
|                     }, | ||||
|                 ], | ||||
|                 needsUrls: [], | ||||
|                 constr( | ||||
|                     state: SpecialVisualizationState, | ||||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|  | @ -1215,6 +1222,7 @@ export default class SpecialVisualizations { | |||
|             { | ||||
|                 funcName: "multi", | ||||
|                 docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering", | ||||
|                 needsUrls: [], | ||||
|                 example: | ||||
|                     "```json\n" + | ||||
|                     JSON.stringify( | ||||
|  | @ -1275,6 +1283,7 @@ export default class SpecialVisualizations { | |||
|                         required: true, | ||||
|                     }, | ||||
|                 ], | ||||
|                 needsUrls: [], | ||||
|                 constr( | ||||
|                     state: SpecialVisualizationState, | ||||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|  |  | |||
|  | @ -37,6 +37,7 @@ export class SubstitutedTranslation extends VariableUiElement { | |||
|                 constr: typeof value === "function" ? value : () => value, | ||||
|                 docs: "Dynamically injected input element", | ||||
|                 args: [], | ||||
|                 needsUrls: [], | ||||
|                 example: "", | ||||
|             }) | ||||
|         }) | ||||
|  |  | |||
|  | @ -566,6 +566,7 @@ class SvgToPdfPage { | |||
|     images: Record<string, HTMLImageElement> = {} | ||||
|     rects: Record<string, { rect: SVGRectElement; isInDef: boolean }> = {} | ||||
|     readonly options: SvgToPdfOptions | ||||
|     public readonly status: UIEventSource<string> | ||||
|     private readonly importedTranslations: Record<string, string> = {} | ||||
|     private readonly layerTranslations: Record<string, Record<string, any>> = {} | ||||
|     /** | ||||
|  | @ -574,7 +575,6 @@ class SvgToPdfPage { | |||
|      */ | ||||
|     private readonly _state: UIEventSource<string> | ||||
|     private _isPrepared = false | ||||
|     public readonly status: UIEventSource<string> | ||||
| 
 | ||||
|     constructor( | ||||
|         page: string, | ||||
|  | @ -674,7 +674,10 @@ class SvgToPdfPage { | |||
|     public async PrepareLanguage(language: string) { | ||||
|         // Always fetch the remote data - it's cached anyway
 | ||||
|         this.layerTranslations[language] = await Utils.downloadJsonCached( | ||||
|             "https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/langs/layers/" + | ||||
|             window.location.protocol + | ||||
|                 "//" + | ||||
|                 window.location.host + | ||||
|                 "/assets/langs/layers/" + | ||||
|                 language + | ||||
|                 ".json", | ||||
|             24 * 60 * 60 * 1000 | ||||
|  | @ -995,6 +998,7 @@ export interface PdfTemplateInfo { | |||
|     orientation: "portrait" | "landscape" | ||||
|     isPublic: boolean | ||||
| } | ||||
| 
 | ||||
| export class SvgToPdf { | ||||
|     public static readonly templates: Record< | ||||
|         "flyer_a4" | "poster_a3" | "poster_a2" | "current_view_a4" | "current_view_a3", | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -3,6 +3,7 @@ import SvelteUIElement from "./src/UI/Base/SvelteUIElement" | |||
| import ThemeViewGUI from "./src/UI/ThemeViewGUI.svelte" | ||||
| import LayoutConfig from "./src/Models/ThemeConfig/LayoutConfig"; | ||||
| import MetaTagging from "./src/Logic/MetaTagging"; | ||||
| import { FixedUiElement } from "./src/UI/Base/FixedUiElement"; | ||||
| 
 | ||||
| function webgl_support() { | ||||
|     try { | ||||
|  |  | |||
							
								
								
									
										57
									
								
								theme.html
									
										
									
									
									
								
							
							
						
						
									
										57
									
								
								theme.html
									
										
									
									
									
								
							|  | @ -4,7 +4,7 @@ | |||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport"> | ||||
|     <!-- CSP // disabled --> | ||||
|     <!-- CSP --> | ||||
|     <link href="./css/mobile.css" rel="stylesheet"/> | ||||
|     <link href="./css/openinghourstable.css" rel="stylesheet"/> | ||||
|     <link href="./css/tagrendering.css" rel="stylesheet"/> | ||||
|  | @ -65,57 +65,12 @@ | |||
|     </div> | ||||
| </div> | ||||
| <div id="belowmap" class="absolute top-0 left-0 -z-10">Below</div> | ||||
| <script src="./src/UI/RemoveOtherLanguages.js"></script> | ||||
| <script async src="./src/InstallServiceWorker.ts" type="module"></script> | ||||
| <script defer src="./src/index.ts" type="module"></script> | ||||
| <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script> | ||||
| 
 | ||||
| <script> | ||||
| 
 | ||||
|     let lang = ((navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage || 'en').substr(0, 2); | ||||
| 
 | ||||
|     function filterLangs(maindiv) { | ||||
|         let foundLangs = 0 | ||||
|         for (const child of Array.from(maindiv.children)) { | ||||
|             if (child.attributes.lang?.value === lang) { | ||||
|                 foundLangs++ | ||||
|             } | ||||
|         } | ||||
|         if (foundLangs === 0) { | ||||
|             lang = "en" | ||||
|         } | ||||
|         for (const child of Array.from(maindiv.children)) { | ||||
|             const childLang = child.attributes.lang | ||||
|             if (childLang === undefined) { | ||||
|                 continue | ||||
|             } | ||||
|             if (childLang.value === lang) { | ||||
|                 continue | ||||
|             } | ||||
|             child.parentElement.removeChild(child) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     filterLangs(document.getElementById("descriptions-while-loading")) | ||||
|     filterLangs(document.getElementById("default-title")) | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <script src="./src/index.ts" type="module"></script> | ||||
| <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script> | ||||
| 
 | ||||
| <script> | ||||
|     window.addEventListener('load', () => { | ||||
|         if ('serviceWorker' in navigator) { | ||||
|             // register service worker | ||||
|             navigator.serviceWorker.register('/service-worker.js').then( | ||||
|                 () => { | ||||
|                     console.log('Service worker registration successful'); | ||||
|                 }, | ||||
|                 err => { | ||||
|                     console.error('Service worker registration failed', err) | ||||
|                 }); | ||||
|         } else { | ||||
|             console.log("Service workers are not supported") | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
|      | ||||
| 
 | ||||
| </body> | ||||
| </html> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue