Refactoring: make needed URLs explicit

This commit is contained in:
Pieter Vander Vennet 2023-09-27 22:21:35 +02:00
parent 7852829f1b
commit 4852888b41
51 changed files with 978 additions and 871 deletions

View file

@ -1658,7 +1658,9 @@
}, },
{ {
"id": "repeated", "id": "repeated",
"labels": ["level"], "labels": [
"level"
],
"condition": "repeat_on~*", "condition": "repeat_on~*",
"render": { "render": {
"en": "Multiple, identical objects can be found on floors {repeat_on}.", "en": "Multiple, identical objects can be found on floors {repeat_on}.",
@ -1667,7 +1669,9 @@
}, },
{ {
"id": "single_level", "id": "single_level",
"labels": ["level"], "labels": [
"level"
],
"condition": "repeat_on=", "condition": "repeat_on=",
"question": { "question": {
"nl": "Op welke verdieping bevindt dit punt zich?", "nl": "Op welke verdieping bevindt dit punt zich?",

View file

@ -1,6 +1,6 @@
{ {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.33.4", "version": "0.33.5",
"repository": "https://github.com/pietervdvn/MapComplete", "repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily", "description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues", "bugs": "https://github.com/pietervdvn/MapComplete/issues",
@ -18,23 +18,10 @@
"Alternatively, you can override the `osm` credentials using the environment variables `VITE_OSM_OAUTH_CLIENT_ID` and `VITE_OSM_OAUTH_SECRET`" "Alternatively, you can override the `osm` credentials using the environment variables `VITE_OSM_OAUTH_CLIENT_ID` and `VITE_OSM_OAUTH_SECRET`"
], ],
"oauth_credentials": { "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", "#": "This client-id is registered by 'MapComplete' on osm.org",
"oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk", "oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk",
"oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg",
"url": "https://www.openstreetmap.org" "url": "https://www.openstreetmap.org"
},
"osm-test": {
"oauth_client_id": "HwUn6GPxGm1m9WwMarxTglhy6dBTM4YkaV1I9h6pDGU",
"oauth_secret": "luFZtPJg7j96K6WM6RpcZ_3M-r6muuDq6fG1ygk0I_4",
"url": "https://master.apis.dev.openstreetmap.org"
}
}, },
"api_keys": { "api_keys": {
"#": "Various API-keys for various services. Feel free to reuse those in another MapComplete-hosted version", "#": "Various API-keys for various services. Feel free to reuse those in another MapComplete-hosted version",

View file

@ -12,8 +12,8 @@ mkdir dist/assets 2> /dev/null
export NODE_OPTIONS="--max-old-space-size=8192" export NODE_OPTIONS="--max-old-space-size=8192"
# This script ends every line with '&&' to chain everything. A failure will thus stop the build # This script ends every line with '&&' to chain everything. A failure will thus stop the build
# npm run generate:editor-layer-index && npm run generate:editor-layer-index &&
# npm run generate && npm run generate &&
npm run generate:layouts npm run generate:layouts
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@ -38,7 +38,8 @@ then
export ASSET_URL export ASSET_URL
echo "$ASSET_URL" echo "$ASSET_URL"
else else
ASSET_URL="$BRANCH" # ASSET_URL="$BRANCH"
ASSET_URL="./"
export ASSET_URL export ASSET_URL
echo "$ASSET_URL" echo "$ASSET_URL"
fi fi

View file

@ -8,6 +8,10 @@ import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig"
import xml2js from "xml2js" import xml2js from "xml2js"
import ScriptUtils from "./ScriptUtils" import ScriptUtils from "./ScriptUtils"
import { Utils } from "../src/Utils" 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"
const sharp = require("sharp") const sharp = require("sharp")
const template = readFileSync("theme.html", "utf8") const template = readFileSync("theme.html", "utf8")
@ -195,29 +199,72 @@ function asLangSpan(t: Translation, tag = "span"): string {
if (lang === "_context") { if (lang === "_context") {
continue continue
} }
values.push(`<${tag} lang='${lang}'>${t.translations[lang]}</${tag}>`) values.push(`<${tag} lang="${lang}">${t.translations[lang]}</${tag}>`)
} }
return values.join("\n") return values.join("\n")
} }
let cspCached: string = undefined let previousSrc: Set<string> = new Set<string>()
function generateCsp(): string { function generateCsp(layout: LayoutConfig): string {
if (cspCached !== undefined) { const apiUrls: string[] = [
return cspCached "self",
...Constants.defaultOverpassUrls,
Constants.countryCoderEndpoint,
"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 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 = { const csp = {
"default-src": "'self'", "default-src": "'self'",
"script-src": "'self'", "script-src": "'self' https://gc.zgo.at/count.js",
"img-src": "*", "img-src": "* data:", // maplibre depends on 'data:' to load
"connect-src": "*", "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) const content = Object.keys(csp)
.map((k) => k + ": " + csp[k]) .map((k) => k + " " + csp[k])
.join("; ") .join("; ")
cspCached = `<meta http-equiv="Content-Security-Policy" content="${content}">` return [
return cspCached `<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")
} }
async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) { async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) {
@ -290,7 +337,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
...apple_icons, ...apple_icons,
].join("\n") ].join("\n")
const loadingText = Translations.t.general.loadingTheme.Subs({ theme: ogTitle }) const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title })
let output = template let output = template
.replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1")) .replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1"))
@ -299,7 +346,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
Translations.t.general.poweredByOsm.textFor(targetLanguage) Translations.t.general.poweredByOsm.textFor(targetLanguage)
) )
.replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific) .replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific)
.replace(/<!-- CSP -->/, generateCsp()) .replace(/<!-- CSP -->/, generateCsp(layout))
.replace( .replace(
/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s, /<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s,
asLangSpan(layout.shortDescription) asLangSpan(layout.shortDescription)
@ -311,7 +358,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
.replace( .replace(
'<script src="./src/index.ts" type="module"></script>', '<script src="./src/index.ts" type="module"></script>',
`<script type="module" src='./index_${layout.id}.ts'></script>` `<script type="module" src="./index_${layout.id}.ts"></script>`
) )
return output return output

View file

@ -4,7 +4,6 @@ hosted.mapcomplete.org {
header { header {
+Permissions-Policy "interest-cohort=()" +Permissions-Policy "interest-cohort=()"
+Report-To `\{"group":"csp-endpoint", "max_age": 86400,"endpoints": [\{"url": "https://report.mapcomplete.org/csp"}], "include_subdomains": true}` +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 ;"
} }
} }

View file

@ -10,14 +10,15 @@
# unzip tiles.zip # unzip tiles.zip
MAPCOMPLETE_CONFIGURATION="config_hetzner" 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 reset:layeroverview
npm run test npm run test
cp config.json config.json.bu &&
cp ./scripts/hetzner/config.json . &&
npm run prepare-deploy && npm run prepare-deploy &&
mv config.json.bu config.json && mv config.json.bu config.json &&
zip dist.zip -r dist/* && zip dist.zip -r dist/* &&
scp -r dist.zip hetzner:/root/ && scp -r dist.zip hetzner:/root/ &&
scp ./scripts/hetzner/config/* 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" rsync -rzh --progress dist.zip hetzner:/root/ &&
ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" &&
rm dist.zip rm dist.zip

View 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)
}
})

View file

@ -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 = { private static providersByName = {
imgur: Imgur.singleton, imgur: Imgur.singleton,
mapillary: Mapillary.singleton, mapillary: Mapillary.singleton,
wikidata: WikidataImageProvider.singleton, wikidata: WikidataImageProvider.singleton,
wikimedia: WikimediaImageProvider.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< private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<
string, string,
UIEventSource<ProvidedImage[]> UIEventSource<ProvidedImage[]>
>() >()
public static byName(name: string) {
return AllImageProviders.providersByName[name.toLowerCase()]
}
public static LoadImagesFor( public static LoadImagesFor(
tags: Store<Record<string, string>>, tags: Store<Record<string, string>>,
tagKey?: string[] tagKey?: string[]

View file

@ -3,6 +3,10 @@ import ImageProvider, { ProvidedImage } from "./ImageProvider"
export default class GenericImageProvider extends ImageProvider { export default class GenericImageProvider extends ImageProvider {
public defaultKeyPrefixes: string[] = ["image"] public defaultKeyPrefixes: string[] = ["image"]
public apiUrls(): string[] {
return []
}
private readonly _valuePrefixBlacklist: string[] private readonly _valuePrefixBlacklist: string[]
public constructor(valuePrefixBlacklist: string[]) { public constructor(valuePrefixBlacklist: string[]) {

View file

@ -65,4 +65,6 @@ export default abstract class ImageProvider {
public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>
public abstract DownloadAttribution(url: string): Promise<LicenseInfo> public abstract DownloadAttribution(url: string): Promise<LicenseInfo>
public abstract apiUrls(): string[]
} }

View file

@ -4,16 +4,23 @@ import { Utils } from "../../Utils";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import { LicenseInfo } from "./LicenseInfo"; import { LicenseInfo } from "./LicenseInfo";
import { ImageUploader } from "./ImageUploader"; import { ImageUploader } from "./ImageUploader";
import Img from "../../UI/Base/Img";
export class Imgur extends ImageProvider implements ImageUploader{ export class Imgur extends ImageProvider implements ImageUploader {
public static readonly defaultValuePrefix = ["https://i.imgur.com"] public static readonly defaultValuePrefix = ["https://i.imgur.com"]
public static readonly singleton = new Imgur() public static readonly singleton = new Imgur()
public readonly defaultKeyPrefixes: string[] = ["image"] public readonly defaultKeyPrefixes: string[] = ["image"]
public readonly maxFileSizeInMegabytes = 10 public readonly maxFileSizeInMegabytes = 10
public static readonly apiUrl = "https://api.imgur.com/3/image"
private constructor() { private constructor() {
super() super()
} }
apiUrls(): string[] {
return [Imgur.apiUrl]
}
/** /**
* Uploads an image, returns the URL where to find the image * Uploads an image, returns the URL where to find the image
* @param title * @param title
@ -24,8 +31,8 @@ export class Imgur extends ImageProvider implements ImageUploader{
title: string, title: string,
description: string, description: string,
blob: File blob: File
): Promise<{ key: string, value: string }> { ): Promise<{ key: string; value: string }> {
const apiUrl = "https://api.imgur.com/3/image" const apiUrl = Imgur.apiUrl
const apiKey = Constants.ImgurApiKey const apiKey = Constants.ImgurApiKey
const formData = new FormData() const formData = new FormData()
@ -33,7 +40,6 @@ export class Imgur extends ImageProvider implements ImageUploader{
formData.append("title", title) formData.append("title", title)
formData.append("description", description) formData.append("description", description)
const settings: RequestInit = { const settings: RequestInit = {
method: "POST", method: "POST",
body: formData, body: formData,

View file

@ -17,6 +17,10 @@ export class Mapillary extends ImageProvider {
] ]
defaultKeyPrefixes = ["mapillary", "image"] defaultKeyPrefixes = ["mapillary", "image"]
apiUrls(): string[] {
return ["https://mapillary.com", "https://www.mapillary.com", "https://graph.mapillary.com"]
}
/** /**
* Indicates that this is the same URL * Indicates that this is the same URL
* Ignores 'stp' parameter * Ignores 'stp' parameter

View file

@ -5,6 +5,9 @@ import { WikimediaImageProvider } from "./WikimediaImageProvider"
import Wikidata from "../Web/Wikidata" import Wikidata from "../Web/Wikidata"
export class WikidataImageProvider extends ImageProvider { export class WikidataImageProvider extends ImageProvider {
public apiUrls(): string[] {
return Wikidata.neededUrls
}
public static readonly singleton = new WikidataImageProvider() public static readonly singleton = new WikidataImageProvider()
public readonly defaultKeyPrefixes = ["wikidata"] public readonly defaultKeyPrefixes = ["wikidata"]

View file

@ -11,11 +11,11 @@ import Wikimedia from "../Web/Wikimedia"
*/ */
export class WikimediaImageProvider extends ImageProvider { export class WikimediaImageProvider extends ImageProvider {
public static readonly singleton = new WikimediaImageProvider() public static readonly singleton = new WikimediaImageProvider()
public static readonly commonsPrefixes = [ public static readonly apiUrls = [
"https://commons.wikimedia.org/wiki/", "https://commons.wikimedia.org/wiki/",
"https://upload.wikimedia.org", "https://upload.wikimedia.org",
"File:",
] ]
public static readonly commonsPrefixes = [...WikimediaImageProvider.apiUrls, "File:"]
private readonly commons_key = "wikimedia_commons" private readonly commons_key = "wikimedia_commons"
public readonly defaultKeyPrefixes = [this.commons_key, "image"] public readonly defaultKeyPrefixes = [this.commons_key, "image"]
@ -66,6 +66,10 @@ export class WikimediaImageProvider extends ImageProvider {
return value return value
} }
apiUrls(): string[] {
return WikimediaImageProvider.apiUrls
}
SourceIcon(backlink: string): BaseUIElement { SourceIcon(backlink: string): BaseUIElement {
const img = Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em") const img = Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em")
if (backlink === undefined) { if (backlink === undefined) {

View file

@ -32,11 +32,12 @@ export default class Maproulette {
private readonly apiKey: string private readonly apiKey: string
public static singleton = new Maproulette() public static singleton = new Maproulette()
public static readonly defaultEndpoint = "https://maproulette.org/api/v2"
/** /**
* Creates a new Maproulette instance * Creates a new Maproulette instance
* @param endpoint The API endpoint to use * @param endpoint The API endpoint to use
*/ */
constructor(endpoint: string = "https://maproulette.org/api/v2") { constructor(endpoint: string = Maproulette.defaultEndpoint) {
this.endpoint = endpoint this.endpoint = endpoint
this.apiKey = Constants.MaprouletteApiKey this.apiKey = Constants.MaprouletteApiKey
} }

View file

@ -0,0 +1,6 @@
export interface AuthConfig {
"#"?: string // optional comment
oauth_client_id: string
oauth_secret: string
url: string
}

File diff suppressed because it is too large Load diff

View file

@ -28,14 +28,8 @@ class FeatureSwitchUtils {
export class OsmConnectionFeatureSwitches { export class OsmConnectionFeatureSwitches {
public readonly featureSwitchFakeUser: UIEventSource<boolean> public readonly featureSwitchFakeUser: UIEventSource<boolean>
public readonly featureSwitchApiURL: UIEventSource<string>
constructor() { 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( this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter(
"fake-user", "fake-user",
@ -143,7 +137,6 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
let testingDefaultValue = false let testingDefaultValue = false
if ( if (
this.featureSwitchApiURL.data !== "osm-test" &&
!Utils.runningFromConsole && !Utils.runningFromConsole &&
(location.hostname === "localhost" || location.hostname === "127.0.0.1") (location.hostname === "localhost" || location.hostname === "127.0.0.1")
) { ) {

View file

@ -1,9 +1,9 @@
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { GeoOperations } from "../GeoOperations" import { GeoOperations } from "../GeoOperations"
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
import { Mapillary } from "../ImageProviders/Mapillary"
import P4C from "pic4carto" import P4C from "pic4carto"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
export interface NearbyImageOptions { export interface NearbyImageOptions {
lon: number lon: number
lat: 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 { export default class NearbyImagesSearch {
private static readonly services = [ public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const
"mapillary", public static readonly apiUrls = ["https://api.flickr.com"]
"flickr", private readonly individualStores: Store<{ images: P4CPicture[]; beforeFilter: number }>[]
"openstreetcam",
"wikicommons",
] as const
private individualStores
private readonly _store: UIEventSource<P4CPicture[]> = new UIEventSource<P4CPicture[]>([]) private readonly _store: UIEventSource<P4CPicture[]> = new UIEventSource<P4CPicture[]>([])
public readonly store: Store<P4CPicture[]> = this._store public readonly store: Store<P4CPicture[]> = this._store
private readonly _options: NearbyImageOptions private readonly _options: NearbyImageOptions
@ -71,16 +66,16 @@ export default class NearbyImagesSearch {
this.update() this.update()
} }
private static buildPictureFetcher( private static async fetchImages(
options: NearbyImageOptions, options: NearbyImageOptions,
fetcher: "mapillary" | "flickr" | "openstreetcam" | "wikicommons" fetcher: P4CService
): Store<{ images: P4CPicture[]; beforeFilter: number }> { ): Promise<P4CPicture[]> {
const picManager = new P4C.PicturesManager({ usefetchers: [fetcher] }) const picManager = new P4C.PicturesManager({ usefetchers: [fetcher] })
const searchRadius = options.searchRadius ?? 100
const maxAgeSeconds = (options.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000 const maxAgeSeconds = (options.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000
const searchRadius = options.searchRadius ?? 100
const p4cStore = Stores.FromPromise<P4CPicture[]>( try {
picManager.startPicsRetrievalAround( const pics: P4CPicture[] = await picManager.startPicsRetrievalAround(
new P4C.LatLng(options.lat, options.lon), new P4C.LatLng(options.lat, options.lon),
searchRadius, searchRadius,
{ {
@ -88,7 +83,21 @@ export default class NearbyImagesSearch {
towardscenter: false, 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( return p4cStore.map(
(images) => { (images) => {
if (images === undefined) { if (images === undefined) {
@ -220,3 +229,5 @@ class ImagesInLoadedDataFetcher {
return foundImages return foundImages
} }
} }
type P4CService = (typeof NearbyImagesSearch.services)[number]

View file

@ -1,7 +1,7 @@
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
export default class PlantNet { export default class PlantNet {
private static baseUrl = public static baseUrl =
"https://my-api.plantnet.org/v2/identify/all?api-key=2b10AAsjzwzJvucA5Ncm5qxe" "https://my-api.plantnet.org/v2/identify/all?api-key=2b10AAsjzwzJvucA5Ncm5qxe"
public static query(imageUrls: string[]): Promise<PlantNetResult> { public static query(imageUrls: string[]): Promise<PlantNetResult> {

View file

@ -123,6 +123,11 @@ export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions {
* Utility functions around wikidata * Utility functions around wikidata
*/ */
export default class 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 _identifierPrefixes = ["Q", "L"].map((str) => str.toLowerCase())
private static readonly _prefixesToRemove = [ private static readonly _prefixesToRemove = [
"https://www.wikidata.org/wiki/Lexeme:", "https://www.wikidata.org/wiki/Lexeme:",
@ -130,11 +135,11 @@ export default class Wikidata {
"http://www.wikidata.org/entity/", "http://www.wikidata.org/entity/",
"Lexeme:", "Lexeme:",
].map((str) => str.toLowerCase()) ].map((str) => str.toLowerCase())
private static readonly _storeCache = new Map< private static readonly _storeCache = new Map<
string, string,
Store<{ success: WikidataResponse } | { error: any }> Store<{ success: WikidataResponse } | { error: any }>
>() >()
/** /**
* Same as LoadWikidataEntry, but wrapped into a UIEventSource * Same as LoadWikidataEntry, but wrapped into a UIEventSource
* @param value * @param value
@ -388,6 +393,7 @@ export default class Wikidata {
} }
private static _cache = new Map<string, Promise<WikidataResponse>>() private static _cache = new Map<string, Promise<WikidataResponse>>()
public static async LoadWikidataEntryAsync(value: string | number): Promise<WikidataResponse> { public static async LoadWikidataEntryAsync(value: string | number): Promise<WikidataResponse> {
const key = "" + value const key = "" + value
const cached = Wikidata._cache.get(key) const cached = Wikidata._cache.get(key)
@ -398,6 +404,7 @@ export default class Wikidata {
Wikidata._cache.set(key, uncached) Wikidata._cache.set(key, uncached)
return uncached return uncached
} }
/** /**
* Loads a wikidata page * Loads a wikidata page
* @returns the entity of the given value * @returns the entity of the given value

View file

@ -34,6 +34,8 @@ export default class Wikipedia {
private static readonly idsToRemove = ["sjabloon_zie"] private static readonly idsToRemove = ["sjabloon_zie"]
public static readonly neededUrls = ["*.wikipedia.org"]
private static readonly _cache = new Map<string, Promise<string>>() private static readonly _cache = new Map<string, Promise<string>>()
private static _fullDetailsCache = new Map<string, Store<FullWikipediaDetails>>() private static _fullDetailsCache = new Map<string, Store<FullWikipediaDetails>>()
public readonly backend: string public readonly backend: string

View file

@ -1,6 +1,7 @@
import * as packagefile from "../../package.json" import * as packagefile from "../../package.json"
import * as extraconfig from "../../config.json" import * as extraconfig from "../../config.json"
import { Utils } from "../Utils" import { Utils } from "../Utils"
import { AuthConfig } from "../Logic/Osm/AuthConfig"
export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number] export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number]
@ -104,7 +105,8 @@ export default class Constants {
public static ImgurApiKey = Constants.config.api_keys.imgur public static ImgurApiKey = Constants.config.api_keys.imgur
public static readonly mapillary_client_token_v4 = Constants.config.api_keys.mapillary_v4 public static readonly mapillary_client_token_v4 = Constants.config.api_keys.mapillary_v4
public static defaultOverpassUrls = Constants.config.default_overpass_urls 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
/** /**
* These are the values that are allowed to use as 'backdrop' icon for a map pin * These are the values that are allowed to use as 'backdrop' icon for a map pin

View file

@ -140,8 +140,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
"oauth_token", "oauth_token",
undefined, undefined,
"Used to complete the login" "Used to complete the login"
), )
osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data,
}) })
this.userRelatedState = new UserRelatedState( this.userRelatedState = new UserRelatedState(
this.osmConnection, this.osmConnection,

View file

@ -22,8 +22,7 @@ export default class AllThemesGui {
"oauth_token", "oauth_token",
undefined, undefined,
"Used to complete the login" "Used to complete the login"
), )
osmConfiguration: <"osm" | "osm-test">featureSwitches.featureSwitchApiURL.data,
}) })
const state = new UserRelatedState(osmConnection) const state = new UserRelatedState(osmConnection)
const intro = new Combine([ const intro = new Combine([

View file

@ -11,6 +11,7 @@ import { Utils } from "../../Utils"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
export class OpenJosm extends Combine { 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) { constructor(osmConnection: OsmConnection, bounds: Store<BBox>, iconStyle?: string) {
const t = Translations.t.general.attribution const t = Translations.t.general.attribution

View file

@ -10,9 +10,11 @@ import Combine from "../Base/Combine"
import Title from "../Base/Title" import Title from "../Base/Title"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import Constants from "../../Models/Constants"
export class AddNoteCommentViz implements SpecialVisualization { export class AddNoteCommentViz implements SpecialVisualization {
funcName = "add_note_comment" 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)." docs = "A textfield to add a comment to a node (with the option to close the note)."
args = [ args = [
{ {

View file

@ -9,7 +9,6 @@ import { Utils } from "../../Utils"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import Loading from "../Base/Loading" import Loading from "../Base/Loading"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Changes } from "../../Logic/Osm/Changes" import { Changes } from "../../Logic/Osm/Changes"
@ -209,6 +208,8 @@ class ApplyButton extends UIElement {
export default class AutoApplyButton implements SpecialVisualization { export default class AutoApplyButton implements SpecialVisualization {
public readonly docs: BaseUIElement public readonly docs: BaseUIElement
public readonly funcName: string = "auto_apply" public readonly funcName: string = "auto_apply"
public readonly needsUrls = []
public readonly args: { public readonly args: {
name: string name: string
defaultValue?: string defaultValue?: string
@ -271,14 +272,7 @@ export default class AutoApplyButton implements SpecialVisualization {
argument: string[] argument: string[]
): BaseUIElement { ): BaseUIElement {
try { try {
if ( if (!state.layout.official && !state.featureSwitchIsTesting.data) {
!state.layout.official &&
!(
state.featureSwitchIsTesting.data ||
state.osmConnection._oauth_config.url ===
OsmConnection.oauth_configs["osm-test"].url
)
) {
const t = Translations.t.general.add.import const t = Translations.t.general.add.import
return new Combine([ return new Combine([
new FixedUiElement( new FixedUiElement(

View file

@ -8,9 +8,11 @@ import Toggle from "../Input/Toggle"
import { LoginToggle } from "./LoginButton" import { LoginToggle } from "./LoginButton"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import Constants from "../../Models/Constants"
export class CloseNoteButton implements SpecialVisualization { export class CloseNoteButton implements SpecialVisualization {
public readonly funcName = "close_note" public readonly funcName = "close_note"
public readonly needsUrls = [Constants.osmAuthConfig.url]
public readonly docs = 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." "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 = [ public readonly args = [

View file

@ -13,7 +13,7 @@ export class ExportAsGpxViz implements SpecialVisualization {
funcName = "export_as_gpx" funcName = "export_as_gpx"
docs = "Exports the selected feature as GPX-file" docs = "Exports the selected feature as GPX-file"
args = [] args = []
needsUrls = []
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,

View file

@ -2,10 +2,13 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import Histogram from "../BigComponents/Histogram" import Histogram from "../BigComponents/Histogram"
import { Feature } from "geojson" import { Feature } from "geojson"
import Constants from "../../Models/Constants"
export class HistogramViz implements SpecialVisualization { export class HistogramViz implements SpecialVisualization {
funcName = "histogram" funcName = "histogram"
docs = "Create a histogram for a list of given values, read from the properties." docs = "Create a histogram for a list of given values, read from the properties."
needsUrls = []
example = example =
'`{histogram(\'some_key\')}` with properties being `{some_key: ["a","b","a","c"]} to create a histogram' '`{histogram(\'some_key\')}` with properties being `{some_key: ["a","b","a","c"]} to create a histogram'
args = [ args = [

View file

@ -24,6 +24,7 @@ export interface ConflateFlowArguments extends ImportFlowArguments {
export default class ConflateImportButtonViz implements SpecialVisualization, AutoAction { export default class ConflateImportButtonViz implements SpecialVisualization, AutoAction {
supportsAutoAction: boolean = true supportsAutoAction: boolean = true
needsUrls = []
public readonly funcName: string = "conflate_button" public readonly funcName: string = "conflate_button"
public readonly args: { public readonly args: {
name: string name: string

View file

@ -194,10 +194,7 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
return { error: t.hasBeenImported } return { error: t.hasBeenImported }
} }
const usesTestUrl = if (!state.layout.official && !isTesting) {
this.state.osmConnection._oauth_config.url ===
OsmConnection.oauth_configs["osm-test"].url
if (!state.layout.official && !(isTesting || usesTestUrl)) {
// Unofficial theme - imports not allowed // Unofficial theme - imports not allowed
return { return {
error: t.officialThemesOnly, error: t.officialThemesOnly,

View file

@ -18,6 +18,7 @@ export class PointImportButtonViz implements SpecialVisualization {
public readonly docs: string | BaseUIElement public readonly docs: string | BaseUIElement
public readonly example?: string public readonly example?: string
public readonly args: { name: string; defaultValue?: string; doc: string }[] public readonly args: { name: string; defaultValue?: string; doc: string }[]
public needsUrls = []
constructor() { constructor() {
this.funcName = "import_button" this.funcName = "import_button"

View file

@ -20,6 +20,7 @@ import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSou
*/ */
export default class WayImportButtonViz implements AutoAction, SpecialVisualization { export default class WayImportButtonViz implements AutoAction, SpecialVisualization {
public readonly funcName: string = "import_way_button" public readonly funcName: string = "import_way_button"
needsUrls = []
public readonly docs: string = 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'" + "This button will copy the data from an external dataset into OpenStreetMap, copying the geometry and adding it as a 'line'" +
ImportFlowUtils.documentationGeneral ImportFlowUtils.documentationGeneral

View file

@ -20,6 +20,7 @@ import { Feature } from "geojson"
export class LanguageElement implements SpecialVisualization { export class LanguageElement implements SpecialVisualization {
funcName: string = "language_chooser" funcName: string = "language_chooser"
needsUrls = []
docs: string | BaseUIElement = docs: string | BaseUIElement =
"The language element allows to show and pick all known (modern) languages. The key can be set" "The language element allows to show and pick all known (modern) languages. The key can be set"

View file

@ -9,6 +9,8 @@ import MapillaryLink from "../BigComponents/MapillaryLink.svelte"
export class MapillaryLinkVis implements SpecialVisualization { export class MapillaryLinkVis implements SpecialVisualization {
funcName = "mapillary_link" funcName = "mapillary_link"
docs = "Adds a button to open mapillary on the specified location" docs = "Adds a button to open mapillary on the specified location"
needsUrls = []
args = [ args = [
{ {
name: "zoom", name: "zoom",

View file

@ -13,6 +13,7 @@ import { BBox } from "../../Logic/BBox"
export class MinimapViz implements SpecialVisualization { export class MinimapViz implements SpecialVisualization {
funcName = "minimap" funcName = "minimap"
docs = "A small map showing the selected feature." docs = "A small map showing the selected feature."
needsUrls = []
args = [ 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", 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",

View file

@ -4,6 +4,7 @@ import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisua
export class MultiApplyViz implements SpecialVisualization { export class MultiApplyViz implements SpecialVisualization {
funcName = "multi_apply" funcName = "multi_apply"
needsUrls = []
docs = 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" "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 = [ args = [

View file

@ -8,9 +8,10 @@ import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import SvelteUIElement from "../Base/SvelteUIElement" import SvelteUIElement from "../Base/SvelteUIElement"
import PlantNet from "../PlantNet/PlantNet.svelte" import PlantNet from "../PlantNet/PlantNet.svelte"
import { default as PlantNetCode } from "../../Logic/Web/PlantNet"
export class PlantNetDetectionViz implements SpecialVisualization { export class PlantNetDetectionViz implements SpecialVisualization {
funcName = "plantnet_detection" funcName = "plantnet_detection"
needsUrls = [PlantNetCode.baseUrl]
docs = 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`). " "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`). "

View file

@ -11,6 +11,8 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
*/ */
export default class QuestionViz implements SpecialVisualization { export default class QuestionViz implements SpecialVisualization {
funcName = "questions" funcName = "questions"
needsUrls = []
docs = docs =
"The special element which shows the questions which are unkown. Added by default if not yet there" "The special element which shows the questions which are unkown. Added by default if not yet there"
args = [ args = [

View file

@ -15,6 +15,7 @@ export class ShareLinkViz implements SpecialVisualization {
doc: "The url to share (default: current URL)", doc: "The url to share (default: current URL)",
}, },
] ]
needsUrls = []
public constr( public constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,

View file

@ -21,6 +21,7 @@ import Maproulette from "../../Logic/Maproulette"
export default class TagApplyButton implements AutoAction, SpecialVisualization { export default class TagApplyButton implements AutoAction, SpecialVisualization {
public readonly funcName = "tag_apply" public readonly funcName = "tag_apply"
needsUrls = []
public readonly docs = 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" + "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 Utils.Special_visualizations_tagsToApplyHelpText

View file

@ -2,6 +2,7 @@ import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
import Constants from "../../Models/Constants"
/** /**
* Wrapper around 'UploadTraceToOsmUI' * Wrapper around 'UploadTraceToOsmUI'
@ -11,6 +12,7 @@ export class UploadToOsmViz implements SpecialVisualization {
docs = docs =
"Uploads the GPS-history as GPX to OpenStreetMap.org; clears the history afterwards. The actual feature is ignored." "Uploads the GPS-history as GPX to OpenStreetMap.org; clears the history afterwards. The actual feature is ignored."
args = [] args = []
needsUrls = [Constants.osmAuthConfig.url]
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,

View file

@ -0,0 +1,32 @@
export {}
let lang = (
(navigator.languages && navigator.languages[0]) ||
navigator.language ||
navigator["userLanguage"] ||
"en"
).substr(0, 2)
function filterLangs(maindiv: HTMLElement) {
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"))

View file

@ -1,118 +1,122 @@
import { Store, UIEventSource } from "../Logic/UIEventSource"; import { Store, UIEventSource } from "../Logic/UIEventSource"
import BaseUIElement from "./BaseUIElement"; import BaseUIElement from "./BaseUIElement"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"; import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection"; import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { Changes } from "../Logic/Osm/Changes"; import { Changes } from "../Logic/Osm/Changes"
import { ExportableMap, MapProperties } from "../Models/MapProperties"; import { ExportableMap, MapProperties } from "../Models/MapProperties"
import LayerState from "../Logic/State/LayerState"; import LayerState from "../Logic/State/LayerState"
import { Feature, Geometry, Point } from "geojson"; import { Feature, Geometry, Point } from "geojson"
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"; import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"; import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import { MenuState } from "../Models/MenuState"; import { MenuState } from "../Models/MenuState"
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
import { RasterLayerPolygon } from "../Models/RasterLayers"; import { RasterLayerPolygon } from "../Models/RasterLayers"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"; import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import { OsmTags } from "../Models/OsmFeature"; import { OsmTags } from "../Models/OsmFeature"
/** /**
* The state needed to render a special Visualisation. * The state needed to render a special Visualisation.
*/ */
export interface SpecialVisualizationState { export interface SpecialVisualizationState {
readonly guistate: MenuState; readonly guistate: MenuState
readonly layout: LayoutConfig; readonly layout: LayoutConfig
readonly featureSwitches: FeatureSwitchState; readonly featureSwitches: FeatureSwitchState
readonly layerState: LayerState; readonly layerState: LayerState
readonly featureProperties: { getStore(id: string): UIEventSource<Record<string, string>>, trackFeature?(feature: { properties: OsmTags }) }; readonly featureProperties: {
getStore(id: string): UIEventSource<Record<string, string>>
trackFeature?(feature: { properties: OsmTags })
}
readonly indexedFeatures: IndexedFeatureSource; readonly indexedFeatures: IndexedFeatureSource
/** /**
* Some features will create a new element that should be displayed. * Some features will create a new element that should be displayed.
* These can be injected by appending them to this featuresource (and pinging it) * These can be injected by appending them to this featuresource (and pinging it)
*/ */
readonly newFeatures: WritableFeatureSource; readonly newFeatures: WritableFeatureSource
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>; readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
readonly osmConnection: OsmConnection; readonly osmConnection: OsmConnection
readonly featureSwitchUserbadge: Store<boolean>; readonly featureSwitchUserbadge: Store<boolean>
readonly featureSwitchIsTesting: Store<boolean>; readonly featureSwitchIsTesting: Store<boolean>
readonly changes: Changes; readonly changes: Changes
readonly osmObjectDownloader: OsmObjectDownloader; readonly osmObjectDownloader: OsmObjectDownloader
/** /**
* State of the main map * State of the main map
*/ */
readonly mapProperties: MapProperties & ExportableMap; readonly mapProperties: MapProperties & ExportableMap
readonly selectedElement: UIEventSource<Feature>; readonly selectedElement: UIEventSource<Feature>
/** /**
* Works together with 'selectedElement' to indicate what properties should be displayed * Works together with 'selectedElement' to indicate what properties should be displayed
*/ */
readonly selectedLayer: UIEventSource<LayerConfig>; readonly selectedLayer: UIEventSource<LayerConfig>
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>; readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
/** /**
* If data is currently being fetched from external sources * If data is currently being fetched from external sources
*/ */
readonly dataIsLoading: Store<boolean>; readonly dataIsLoading: Store<boolean>
/** /**
* Only needed for 'ReplaceGeometryAction' * Only needed for 'ReplaceGeometryAction'
*/ */
readonly fullNodeDatabase?: FullNodeDatabaseSource; readonly fullNodeDatabase?: FullNodeDatabaseSource
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>; readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
readonly userRelatedState: { readonly userRelatedState: {
readonly imageLicense: UIEventSource<string>; readonly imageLicense: UIEventSource<string>
readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
readonly mangroveIdentity: MangroveIdentity readonly mangroveIdentity: MangroveIdentity
readonly showAllQuestionsAtOnce: UIEventSource<boolean> readonly showAllQuestionsAtOnce: UIEventSource<boolean>
readonly preferencesAsTags: Store<Record<string, string>> readonly preferencesAsTags: Store<Record<string, string>>
readonly language: UIEventSource<string> readonly language: UIEventSource<string>
}; }
readonly lastClickObject: WritableFeatureSource; readonly lastClickObject: WritableFeatureSource
readonly availableLayers: Store<RasterLayerPolygon[]>; readonly availableLayers: Store<RasterLayerPolygon[]>
readonly imageUploadManager: ImageUploadManager; readonly imageUploadManager: ImageUploadManager
} }
export interface SpecialVisualization { export interface SpecialVisualization {
readonly funcName: string; readonly funcName: string
readonly docs: string | BaseUIElement; readonly docs: string | BaseUIElement
readonly example?: string; readonly example?: string
readonly needsUrls: string[]
/** /**
* Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included * Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included
*/ */
readonly needsNodeDatabase?: boolean; readonly needsNodeDatabase?: boolean
readonly args: { readonly args: {
name: string name: string
defaultValue?: string defaultValue?: string
doc: string doc: string
required?: false | boolean required?: false | boolean
}[]; }[]
readonly getLayerDependencies?: (argument: string[]) => string[]; readonly getLayerDependencies?: (argument: string[]) => string[]
structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[]; structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[]
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig
): BaseUIElement; ): BaseUIElement
} }
export type RenderingSpecification = export type RenderingSpecification =
| string | string
| { | {
func: SpecialVisualization func: SpecialVisualization
args: string[] args: string[]
style: string style: string
} }

View file

@ -1,71 +1,79 @@
import Combine from "./Base/Combine"; import Combine from "./Base/Combine"
import { FixedUiElement } from "./Base/FixedUiElement"; import { FixedUiElement } from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement"; import BaseUIElement from "./BaseUIElement"
import Title from "./Base/Title"; import Title from "./Base/Title"
import Table from "./Base/Table"; import Table from "./Base/Table"
import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"; import {
import { HistogramViz } from "./Popup/HistogramViz"; RenderingSpecification,
import { MinimapViz } from "./Popup/MinimapViz"; SpecialVisualization,
import { ShareLinkViz } from "./Popup/ShareLinkViz"; SpecialVisualizationState,
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"; } from "./SpecialVisualization"
import { MultiApplyViz } from "./Popup/MultiApplyViz"; import { HistogramViz } from "./Popup/HistogramViz"
import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz"; import { MinimapViz } from "./Popup/MinimapViz"
import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"; import { ShareLinkViz } from "./Popup/ShareLinkViz"
import TagApplyButton from "./Popup/TagApplyButton"; import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
import { CloseNoteButton } from "./Popup/CloseNoteButton"; import { MultiApplyViz } from "./Popup/MultiApplyViz"
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"; import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz"
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"; import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"; import TagApplyButton from "./Popup/TagApplyButton"
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"; import { CloseNoteButton } from "./Popup/CloseNoteButton"
import { ImageCarousel } from "./Image/ImageCarousel"; import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
import { VariableUiElement } from "./Base/VariableUIElement"; import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
import { Utils } from "../Utils"; import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"; import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
import { Translation } from "./i18n/Translation"; import { ImageCarousel } from "./Image/ImageCarousel"
import Translations from "./i18n/Translations"; import { VariableUiElement } from "./Base/VariableUIElement"
import ReviewForm from "./Reviews/ReviewForm"; import { Utils } from "../Utils"
import ReviewElement from "./Reviews/ReviewElement"; import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"; import { Translation } from "./i18n/Translation"
import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; import Translations from "./i18n/Translations"
import { SubtleButton } from "./Base/SubtleButton"; import ReviewForm from "./Reviews/ReviewForm"
import Svg from "../Svg"; import ReviewElement from "./Reviews/ReviewElement"
import NoteCommentElement from "./Popup/NoteCommentElement"; import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
import { SubstitutedTranslation } from "./SubstitutedTranslation"; import { SubtleButton } from "./Base/SubtleButton"
import List from "./Base/List"; import Svg from "../Svg"
import StatisticsPanel from "./BigComponents/StatisticsPanel"; import NoteCommentElement from "./Popup/NoteCommentElement"
import AutoApplyButton from "./Popup/AutoApplyButton"; import { SubstitutedTranslation } from "./SubstitutedTranslation"
import { LanguageElement } from "./Popup/LanguageElement"; import List from "./Base/List"
import FeatureReviews from "../Logic/Web/MangroveReviews"; import StatisticsPanel from "./BigComponents/StatisticsPanel"
import Maproulette from "../Logic/Maproulette"; import AutoApplyButton from "./Popup/AutoApplyButton"
import SvelteUIElement from "./Base/SvelteUIElement"; import { LanguageElement } from "./Popup/LanguageElement"
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"; import FeatureReviews from "../Logic/Web/MangroveReviews"
import QuestionViz from "./Popup/QuestionViz"; import Maproulette from "../Logic/Maproulette"
import { Feature, Point } from "geojson"; import SvelteUIElement from "./Base/SvelteUIElement"
import { GeoOperations } from "../Logic/GeoOperations"; import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import CreateNewNote from "./Popup/CreateNewNote.svelte"; import QuestionViz from "./Popup/QuestionViz"
import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"; import { Feature, Point } from "geojson"
import UserProfile from "./BigComponents/UserProfile.svelte"; import { GeoOperations } from "../Logic/GeoOperations"
import LanguagePicker from "./LanguagePicker"; import CreateNewNote from "./Popup/CreateNewNote.svelte"
import Link from "./Base/Link"; import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import UserProfile from "./BigComponents/UserProfile.svelte"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; import LanguagePicker from "./LanguagePicker"
import { OsmTags, WayId } from "../Models/OsmFeature"; import Link from "./Base/Link"
import MoveWizard from "./Popup/MoveWizard"; import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import SplitRoadWizard from "./Popup/SplitRoadWizard"; import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"; import { OsmTags, WayId } from "../Models/OsmFeature"
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"; import MoveWizard from "./Popup/MoveWizard"
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"; import SplitRoadWizard from "./Popup/SplitRoadWizard"
import { PointImportButtonViz } from "./Popup/ImportButtons/PointImportButtonViz"; import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"
import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz"; import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz"; import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"
import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte"; import { PointImportButtonViz } from "./Popup/ImportButtons/PointImportButtonViz"
import { OpenJosm } from "./BigComponents/OpenJosm"; import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"; import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz"
import FediverseValidator from "./InputElement/Validators/FediverseValidator"; import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte"
import SendEmail from "./Popup/SendEmail.svelte"; import { OpenJosm } from "./BigComponents/OpenJosm"
import NearbyImages from "./Popup/NearbyImages.svelte"; import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte"; import FediverseValidator from "./InputElement/Validators/FediverseValidator"
import UploadImage from "./Image/UploadImage.svelte"; 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"
class NearbyImageVis implements SpecialVisualization { class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -79,7 +87,7 @@ class NearbyImageVis implements SpecialVisualization {
docs = 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" "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" funcName = "nearby_images"
needsUrls = NearbyImagesSearch.apiUrls
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
@ -117,6 +125,7 @@ class StealViz implements SpecialVisualization {
required: true, required: true,
}, },
] ]
needsUrls = []
constr(state: SpecialVisualizationState, featureTags, args) { constr(state: SpecialVisualizationState, featureTags, args) {
const [featureIdKey, layerAndtagRenderingIds] = args const [featureIdKey, layerAndtagRenderingIds] = args
@ -264,7 +273,6 @@ export default class SpecialVisualizations {
SpecialVisualizations.specialVisualizations SpecialVisualizations.specialVisualizations
.map((sp) => sp.funcName + "()") .map((sp) => sp.funcName + "()")
.join(", ") .join(", ")
} }
} }
@ -378,6 +386,7 @@ export default class SpecialVisualizations {
funcName: "add_new_point", 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`", docs: "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`",
args: [], args: [],
needsUrls: [],
constr(state: SpecialVisualizationState, _, __, feature): BaseUIElement { constr(state: SpecialVisualizationState, _, __, feature): BaseUIElement {
let [lon, lat] = GeoOperations.centerpointCoordinates(feature) let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(AddNewPoint, { return new SvelteUIElement(AddNewPoint, {
@ -389,6 +398,7 @@ export default class SpecialVisualizations {
{ {
funcName: "user_profile", funcName: "user_profile",
args: [], 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'", 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 { constr(state: SpecialVisualizationState): BaseUIElement {
return new SvelteUIElement(UserProfile, { return new SvelteUIElement(UserProfile, {
@ -399,6 +409,7 @@ export default class SpecialVisualizations {
{ {
funcName: "language_picker", funcName: "language_picker",
args: [], args: [],
needsUrls: [],
docs: "A component to set the language of the user interface", docs: "A component to set the language of the user interface",
constr(state: SpecialVisualizationState): BaseUIElement { constr(state: SpecialVisualizationState): BaseUIElement {
return new LanguagePicker( return new LanguagePicker(
@ -410,6 +421,7 @@ export default class SpecialVisualizations {
{ {
funcName: "logout", funcName: "logout",
args: [], args: [],
needsUrls: [Constants.osmAuthConfig.url],
docs: "Shows a button where the user can log out", docs: "Shows a button where the user can log out",
constr(state: SpecialVisualizationState): BaseUIElement { constr(state: SpecialVisualizationState): BaseUIElement {
return new SubtleButton(Svg.logout_svg(), Translations.t.general.logout, { return new SubtleButton(Svg.logout_svg(), Translations.t.general.logout, {
@ -426,6 +438,7 @@ export default class SpecialVisualizations {
funcName: "split_button", funcName: "split_button",
docs: "Adds a button which allows to split a way", docs: "Adds a button which allows to split a way",
args: [], args: [],
needsUrls: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>> tagSource: UIEventSource<Record<string, string>>
@ -446,6 +459,7 @@ export default class SpecialVisualizations {
funcName: "move_button", 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", docs: "Adds a button which allows to move the object to another location. The config will be read from the layer config",
args: [], args: [],
needsUrls: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
@ -469,6 +483,7 @@ export default class SpecialVisualizations {
funcName: "delete_button", 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", docs: "Adds a button which allows to delete the object at this location. The config will be read from the layer config",
args: [], args: [],
needsUrls: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
@ -493,6 +508,7 @@ export default class SpecialVisualizations {
{ {
funcName: "open_note", funcName: "open_note",
args: [], 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", 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( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
@ -525,6 +541,7 @@ export default class SpecialVisualizations {
defaultValue: "wikidata;wikipedia", defaultValue: "wikidata;wikipedia",
}, },
], ],
needsUrls: [...Wikidata.neededUrls, ...Wikipedia.neededUrls],
example: 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", "`{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) => { constr: (_, tagsSource, args) => {
@ -548,6 +565,7 @@ export default class SpecialVisualizations {
defaultValue: "wikidata", defaultValue: "wikidata",
}, },
], ],
needsUrls: Wikidata.neededUrls,
example: example:
"`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself", "`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself",
constr: (_, tagsSource, args) => constr: (_, tagsSource, args) =>
@ -577,6 +595,7 @@ export default class SpecialVisualizations {
funcName: "all_tags", funcName: "all_tags",
docs: "Prints all key-value pairs of the object - used for debugging", docs: "Prints all key-value pairs of the object - used for debugging",
args: [], args: [],
needsUrls: [],
constr: (state, tags: UIEventSource<any>) => constr: (state, tags: UIEventSource<any>) =>
new SvelteUIElement(AllTagsPanel, { tags, state }), new SvelteUIElement(AllTagsPanel, { tags, state }),
}, },
@ -590,6 +609,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 ", 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) => { constr: (state, tags, args) => {
let imagePrefixes: string[] = undefined let imagePrefixes: string[] = undefined
if (args.length > 0) { if (args.length > 0) {
@ -605,27 +625,32 @@ export default class SpecialVisualizations {
{ {
funcName: "image_upload", funcName: "image_upload",
docs: "Creates a button where a user can upload an image to IMGUR", docs: "Creates a button where a user can upload an image to IMGUR",
needsUrls: [Imgur.apiUrl],
args: [ args: [
{ {
name: "image-key", name: "image-key",
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
required: false required: false,
}, },
{ {
name: "label", name: "label",
doc: "The text to show on the button", doc: "The text to show on the button",
required: false required: false,
}, },
], ],
constr: (state, tags, args) => { constr: (state, tags, args) => {
return new SvelteUIElement(UploadImage, { return new SvelteUIElement(UploadImage, {
state,tags, labelText: args[1], image: args[0] state,
tags,
labelText: args[1],
image: args[0],
}) })
}, },
}, },
{ {
funcName: "reviews", funcName: "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", 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: 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", "`{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: [ args: [
@ -676,6 +701,7 @@ export default class SpecialVisualizations {
doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__", doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__",
}, },
], ],
needsUrls: [],
example: 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)}`", "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) => { constr: (state, tagSource: UIEventSource<any>, args) => {
@ -688,38 +714,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", 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. ", 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: example:
"If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ...", "If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ...",
@ -757,6 +754,7 @@ export default class SpecialVisualizations {
funcName: "export_as_geojson", funcName: "export_as_geojson",
docs: "Exports the selected feature as GeoJson-file", docs: "Exports the selected feature as GeoJson-file",
args: [], args: [],
needsUrls: [],
constr: (state, tagSource, tagsSource, feature, layer) => { constr: (state, tagSource, tagsSource, feature, layer) => {
const t = Translations.t.general.download const t = Translations.t.general.download
@ -786,6 +784,7 @@ export default class SpecialVisualizations {
funcName: "open_in_iD", funcName: "open_in_iD",
docs: "Opens the current view in the iD-editor", docs: "Opens the current view in the iD-editor",
args: [], args: [],
needsUrls: [],
constr: (state, feature) => { constr: (state, feature) => {
return new SvelteUIElement(OpenIdEditor, { return new SvelteUIElement(OpenIdEditor, {
mapProperties: state.mapProperties, mapProperties: state.mapProperties,
@ -797,6 +796,8 @@ export default class SpecialVisualizations {
funcName: "open_in_josm", funcName: "open_in_josm",
docs: "Opens the current view in the JOSM-editor", docs: "Opens the current view in the JOSM-editor",
args: [], args: [],
needsUrls: OpenJosm.needsUrls,
constr: (state) => { constr: (state) => {
return new OpenJosm(state.osmConnection, state.mapProperties.bounds) return new OpenJosm(state.osmConnection, state.mapProperties.bounds)
}, },
@ -805,6 +806,7 @@ export default class SpecialVisualizations {
funcName: "clear_location_history", funcName: "clear_location_history",
docs: "A button to remove the travelled track information from the device", docs: "A button to remove the travelled track information from the device",
args: [], args: [],
needsUrls: [],
constr: (state) => { constr: (state) => {
return new SubtleButton( return new SubtleButton(
Svg.delete_icon_svg().SetStyle("height: 1.5rem"), Svg.delete_icon_svg().SetStyle("height: 1.5rem"),
@ -830,6 +832,7 @@ export default class SpecialVisualizations {
defaultValue: "0", defaultValue: "0",
}, },
], ],
needsUrls: [Constants.osmAuthConfig.url],
constr: (state, tags, args) => constr: (state, tags, args) =>
new VariableUiElement( new VariableUiElement(
tags tags
@ -858,16 +861,18 @@ export default class SpecialVisualizations {
defaultValue: "id", defaultValue: "id",
}, },
], ],
needsUrls: [Imgur.apiUrl],
constr: (state, tags, args) => { constr: (state, tags, args) => {
const id = tags.data[args[0] ?? "id"] const id = tags.data[args[0] ?? "id"]
tags = state.featureProperties.getStore(id) tags = state.featureProperties.getStore(id)
console.log("Id is", id) console.log("Id is", id)
return new SvelteUIElement(UploadImage, {state, tags}) return new SvelteUIElement(UploadImage, { state, tags })
} },
}, },
{ {
funcName: "title", funcName: "title",
args: [], args: [],
needsUrls: [],
docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'", docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'",
example: example:
"`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.", "`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.",
@ -888,6 +893,7 @@ export default class SpecialVisualizations {
{ {
funcName: "maproulette_task", funcName: "maproulette_task",
args: [], args: [],
needsUrls: [Maproulette.defaultEndpoint],
constr(state, tagSource) { constr(state, tagSource) {
let parentId = tagSource.data.mr_challengeId let parentId = tagSource.data.mr_challengeId
if (parentId === undefined) { if (parentId === undefined) {
@ -931,6 +937,7 @@ export default class SpecialVisualizations {
{ {
funcName: "maproulette_set_status", funcName: "maproulette_set_status",
docs: "Change the status of the given MapRoulette task", docs: "Change the status of the given MapRoulette task",
needsUrls: [Maproulette.defaultEndpoint],
example: example:
" The following example sets the status to '2' (false positive)\n" + " The following example sets the status to '2' (false positive)\n" +
"\n" + "\n" +
@ -1054,6 +1061,7 @@ export default class SpecialVisualizations {
funcName: "statistics", funcName: "statistics",
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer", docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
args: [], args: [],
needsUrls: [],
constr: (state) => { constr: (state) => {
return new Combine( return new Combine(
state.layout.layers state.layout.layers
@ -1096,6 +1104,8 @@ export default class SpecialVisualizations {
required: true, required: true,
}, },
], ],
needsUrls: [],
constr(__, tags, args) { constr(__, tags, args) {
return new SvelteUIElement(SendEmail, { args, tags }) return new SvelteUIElement(SendEmail, { args, tags })
}, },
@ -1123,6 +1133,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", 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( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
@ -1144,6 +1155,7 @@ export default class SpecialVisualizations {
{ {
funcName: "multi", 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", 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: example:
"```json\n" + "```json\n" +
JSON.stringify( JSON.stringify(
@ -1204,6 +1216,7 @@ export default class SpecialVisualizations {
required: true, required: true,
}, },
], ],
needsUrls: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,

View file

@ -37,6 +37,7 @@ export class SubstitutedTranslation extends VariableUiElement {
constr: typeof value === "function" ? value : () => value, constr: typeof value === "function" ? value : () => value,
docs: "Dynamically injected input element", docs: "Dynamically injected input element",
args: [], args: [],
needsUrls: [],
example: "", example: "",
}) })
}) })

File diff suppressed because one or more lines are too long

View file

@ -3,6 +3,7 @@ import SvelteUIElement from "./src/UI/Base/SvelteUIElement"
import ThemeViewGUI from "./src/UI/ThemeViewGUI.svelte" import ThemeViewGUI from "./src/UI/ThemeViewGUI.svelte"
import LayoutConfig from "./src/Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "./src/Models/ThemeConfig/LayoutConfig";
import MetaTagging from "./src/Logic/MetaTagging"; import MetaTagging from "./src/Logic/MetaTagging";
import { FixedUiElement } from "./src/UI/Base/FixedUiElement";
function webgl_support() { function webgl_support() {
try { try {

View file

@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport"> <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/mobile.css" rel="stylesheet"/>
<link href="./css/openinghourstable.css" rel="stylesheet"/> <link href="./css/openinghourstable.css" rel="stylesheet"/>
<link href="./css/tagrendering.css" rel="stylesheet"/> <link href="./css/tagrendering.css" rel="stylesheet"/>
@ -65,57 +65,12 @@
</div> </div>
</div> </div>
<div id="belowmap" class="absolute top-0 left-0 -z-10">Below</div> <div id="belowmap" class="absolute top-0 left-0 -z-10">Below</div>
<script async src="./src/UI/RemoveOtherLanguages.ts" type="module"></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> </body>
</html> </html>