Merge branch 'security/strict-csp' into develop

This commit is contained in:
Pieter Vander Vennet 2023-09-30 15:41:32 +02:00
commit 394bad545d
54 changed files with 358 additions and 209 deletions

1
.gitignore vendored
View file

@ -39,3 +39,4 @@ service-worker.js
*.vsix
public/*.webmanifest
public/assets/generated/
public/assets/langs/*

12
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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=""

View file

@ -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

View file

@ -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
}
}

View file

@ -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

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 = {
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[]

View file

@ -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[]) {

View file

@ -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[]
}

View file

@ -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()

View file

@ -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

View file

@ -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"]

View file

@ -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) {

View file

@ -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
}

View file

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

View file

@ -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

View file

@ -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

View file

@ -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")
) {

View file

@ -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]

View file

@ -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> {

View file

@ -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

View file

@ -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

View file

@ -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
*/

View file

@ -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,

View file

@ -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([

View file

@ -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

View file

@ -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 = [
{

View file

@ -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(

View file

@ -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 = [

View file

@ -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>>,

View file

@ -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 = [

View file

@ -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

View file

@ -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,

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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",

View file

@ -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",

View file

@ -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 = [

View file

@ -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`). "

View file

@ -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 = [

View file

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

View file

@ -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

View file

@ -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,

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

View file

@ -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

View file

@ -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>>,

View file

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

View file

@ -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

View file

@ -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 {

View file

@ -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>