forked from MapComplete/MapComplete
Refactoring: make needed URLs explicit
This commit is contained in:
parent
7852829f1b
commit
4852888b41
51 changed files with 978 additions and 871 deletions
|
@ -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?",
|
||||||
|
|
15
package.json
15
package.json
|
@ -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,24 +18,11 @@
|
||||||
"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",
|
||||||
"imgur": "7070e7167f0a25a",
|
"imgur": "7070e7167f0a25a",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 ;"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
13
src/InstallServiceWorker.ts
Normal file
13
src/InstallServiceWorker.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export {}
|
||||||
|
window.addEventListener("load", async () => {
|
||||||
|
if (!("serviceWorker" in navigator)) {
|
||||||
|
console.log("Service workers are not supported")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.serviceWorker.register("/service-worker.js")
|
||||||
|
console.log("Service worker registration successful")
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Service worker registration failed", err)
|
||||||
|
}
|
||||||
|
})
|
|
@ -23,27 +23,27 @@ export default class AllImageProviders {
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
public static apiUrls: string[] = [].concat(
|
||||||
|
...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls())
|
||||||
|
)
|
||||||
|
public static defaultKeys = [].concat(
|
||||||
|
AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes)
|
||||||
|
)
|
||||||
private static providersByName = {
|
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[]
|
||||||
|
|
|
@ -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[]) {
|
||||||
|
|
|
@ -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[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
6
src/Logic/Osm/AuthConfig.ts
Normal file
6
src/Logic/Osm/AuthConfig.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export interface AuthConfig {
|
||||||
|
"#"?: string // optional comment
|
||||||
|
oauth_client_id: string
|
||||||
|
oauth_secret: string
|
||||||
|
url: string
|
||||||
|
}
|
|
@ -1,63 +1,55 @@
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { osmAuth } from "osm-auth";
|
import { osmAuth } from "osm-auth"
|
||||||
import { Store, Stores, UIEventSource } from "../UIEventSource";
|
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||||
import { OsmPreferences } from "./OsmPreferences";
|
import { OsmPreferences } from "./OsmPreferences"
|
||||||
import { Utils } from "../../Utils";
|
import { Utils } from "../../Utils"
|
||||||
import { LocalStorageSource } from "../Web/LocalStorageSource";
|
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||||
import * as config from "../../../package.json";
|
import { AuthConfig } from "./AuthConfig"
|
||||||
|
import Constants from "../../Models/Constants"
|
||||||
|
|
||||||
export default class UserDetails {
|
export default class UserDetails {
|
||||||
public loggedIn = false;
|
public loggedIn = false
|
||||||
public name = "Not logged in";
|
public name = "Not logged in"
|
||||||
public uid: number;
|
public uid: number
|
||||||
public csCount = 0;
|
public csCount = 0
|
||||||
public img?: string;
|
public img?: string
|
||||||
public unreadMessages = 0;
|
public unreadMessages = 0
|
||||||
public totalMessages: number = 0;
|
public totalMessages: number = 0
|
||||||
public home: { lon: number; lat: number };
|
public home: { lon: number; lat: number }
|
||||||
public backend: string;
|
public backend: string
|
||||||
public account_created: string;
|
public account_created: string
|
||||||
public tracesCount: number = 0;
|
public tracesCount: number = 0
|
||||||
public description: string;
|
public description: string
|
||||||
|
|
||||||
constructor(backend: string) {
|
constructor(backend: string) {
|
||||||
this.backend = backend;
|
this.backend = backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthConfig {
|
|
||||||
"#"?: string; // optional comment
|
|
||||||
oauth_client_id: string;
|
|
||||||
oauth_secret: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
|
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
|
||||||
|
|
||||||
export class OsmConnection {
|
export class OsmConnection {
|
||||||
public static readonly oauth_configs: Record<string, AuthConfig> =
|
public auth
|
||||||
config.config.oauth_credentials;
|
public userDetails: UIEventSource<UserDetails>
|
||||||
public auth;
|
public isLoggedIn: Store<boolean>
|
||||||
public userDetails: UIEventSource<UserDetails>;
|
|
||||||
public isLoggedIn: Store<boolean>;
|
|
||||||
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
|
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
|
||||||
"unknown"
|
"unknown"
|
||||||
);
|
)
|
||||||
public apiIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
|
public apiIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
|
||||||
"unknown"
|
"unknown"
|
||||||
);
|
)
|
||||||
|
|
||||||
public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">(
|
public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">(
|
||||||
"not-attempted"
|
"not-attempted"
|
||||||
);
|
)
|
||||||
public preferencesHandler: OsmPreferences;
|
public preferencesHandler: OsmPreferences
|
||||||
public readonly _oauth_config: AuthConfig;
|
public readonly _oauth_config: AuthConfig
|
||||||
private readonly _dryRun: Store<boolean>;
|
private readonly _dryRun: Store<boolean>
|
||||||
private fakeUser: boolean;
|
private readonly fakeUser: boolean
|
||||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []
|
||||||
private readonly _iframeMode: Boolean | boolean;
|
private readonly _iframeMode: Boolean | boolean
|
||||||
private readonly _singlePage: boolean;
|
private readonly _singlePage: boolean
|
||||||
private isChecking = false;
|
private isChecking = false
|
||||||
|
|
||||||
constructor(options?: {
|
constructor(options?: {
|
||||||
dryRun?: Store<boolean>
|
dryRun?: Store<boolean>
|
||||||
|
@ -65,86 +57,83 @@ export class OsmConnection {
|
||||||
oauth_token?: UIEventSource<string>
|
oauth_token?: UIEventSource<string>
|
||||||
// Used to keep multiple changesets open and to write to the correct changeset
|
// Used to keep multiple changesets open and to write to the correct changeset
|
||||||
singlePage?: boolean
|
singlePage?: boolean
|
||||||
osmConfiguration?: "osm" | "osm-test"
|
|
||||||
attemptLogin?: true | boolean
|
attemptLogin?: true | boolean
|
||||||
}) {
|
}) {
|
||||||
options = options ?? {};
|
options ??= {}
|
||||||
this.fakeUser = options.fakeUser ?? false;
|
this.fakeUser = options?.fakeUser ?? false
|
||||||
this._singlePage = options.singlePage ?? true;
|
this._singlePage = options?.singlePage ?? true
|
||||||
this._oauth_config =
|
this._oauth_config = Constants.osmAuthConfig
|
||||||
OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ??
|
console.debug("Using backend", this._oauth_config.url)
|
||||||
OsmConnection.oauth_configs.osm;
|
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top
|
||||||
console.debug("Using backend", this._oauth_config.url);
|
|
||||||
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
|
|
||||||
|
|
||||||
// Check if there are settings available in environment variables, and if so, use those
|
// Check if there are settings available in environment variables, and if so, use those
|
||||||
if (
|
if (
|
||||||
import.meta.env.VITE_OSM_OAUTH_CLIENT_ID !== undefined &&
|
import.meta.env.VITE_OSM_OAUTH_CLIENT_ID !== undefined &&
|
||||||
import.meta.env.VITE_OSM_OAUTH_SECRET !== undefined
|
import.meta.env.VITE_OSM_OAUTH_SECRET !== undefined
|
||||||
) {
|
) {
|
||||||
console.debug("Using environment variables for oauth config");
|
console.debug("Using environment variables for oauth config")
|
||||||
this._oauth_config = {
|
this._oauth_config = {
|
||||||
oauth_client_id: import.meta.env.VITE_OSM_OAUTH_CLIENT_ID,
|
oauth_client_id: import.meta.env.VITE_OSM_OAUTH_CLIENT_ID,
|
||||||
oauth_secret: import.meta.env.VITE_OSM_OAUTH_SECRET,
|
oauth_secret: import.meta.env.VITE_OSM_OAUTH_SECRET,
|
||||||
url: "https://api.openstreetmap.org"
|
url: "https://api.openstreetmap.org",
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userDetails = new UIEventSource<UserDetails>(
|
this.userDetails = new UIEventSource<UserDetails>(
|
||||||
new UserDetails(this._oauth_config.url),
|
new UserDetails(this._oauth_config.url),
|
||||||
"userDetails"
|
"userDetails"
|
||||||
);
|
)
|
||||||
if (options.fakeUser) {
|
if (options.fakeUser) {
|
||||||
const ud = this.userDetails.data;
|
const ud = this.userDetails.data
|
||||||
ud.csCount = 5678;
|
ud.csCount = 5678
|
||||||
ud.loggedIn = true;
|
ud.loggedIn = true
|
||||||
ud.unreadMessages = 0;
|
ud.unreadMessages = 0
|
||||||
ud.name = "Fake user";
|
ud.name = "Fake user"
|
||||||
ud.totalMessages = 42;
|
ud.totalMessages = 42
|
||||||
}
|
}
|
||||||
const self = this;
|
const self = this
|
||||||
this.UpdateCapabilities();
|
this.UpdateCapabilities()
|
||||||
this.isLoggedIn = this.userDetails.map(
|
this.isLoggedIn = this.userDetails.map(
|
||||||
(user) =>
|
(user) =>
|
||||||
user.loggedIn &&
|
user.loggedIn &&
|
||||||
(self.apiIsOnline.data === "unknown" || self.apiIsOnline.data === "online"),
|
(self.apiIsOnline.data === "unknown" || self.apiIsOnline.data === "online"),
|
||||||
[this.apiIsOnline]
|
[this.apiIsOnline]
|
||||||
);
|
)
|
||||||
this.isLoggedIn.addCallback((isLoggedIn) => {
|
this.isLoggedIn.addCallback((isLoggedIn) => {
|
||||||
if (self.userDetails.data.loggedIn == false && isLoggedIn == true) {
|
if (self.userDetails.data.loggedIn == false && isLoggedIn == true) {
|
||||||
// We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do
|
// We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do
|
||||||
// This means someone attempted to toggle this; so we attempt to login!
|
// This means someone attempted to toggle this; so we attempt to login!
|
||||||
self.AttemptLogin();
|
self.AttemptLogin()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false);
|
this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false)
|
||||||
|
|
||||||
this.updateAuthObject();
|
this.updateAuthObject()
|
||||||
|
|
||||||
this.preferencesHandler = new OsmPreferences(
|
this.preferencesHandler = new OsmPreferences(
|
||||||
this.auth,
|
this.auth,
|
||||||
<any /*This is needed to make the tests work*/>this
|
<any /*This is needed to make the tests work*/>this
|
||||||
);
|
)
|
||||||
|
|
||||||
if (options.oauth_token?.data !== undefined) {
|
if (options.oauth_token?.data !== undefined) {
|
||||||
console.log(options.oauth_token.data);
|
console.log(options.oauth_token.data)
|
||||||
const self = this;
|
const self = this
|
||||||
this.auth.bootstrapToken(
|
this.auth.bootstrapToken(
|
||||||
options.oauth_token.data,
|
options.oauth_token.data,
|
||||||
(x) => {
|
(x) => {
|
||||||
console.log("Called back: ", x);
|
console.log("Called back: ", x)
|
||||||
self.AttemptLogin();
|
self.AttemptLogin()
|
||||||
},
|
},
|
||||||
this.auth
|
this.auth
|
||||||
);
|
)
|
||||||
|
|
||||||
options.oauth_token.setData(undefined);
|
options.oauth_token.setData(undefined)
|
||||||
}
|
}
|
||||||
if (this.auth.authenticated() && options.attemptLogin !== false) {
|
if (this.auth.authenticated() && options.attemptLogin !== false) {
|
||||||
this.AttemptLogin(); // Also updates the user badge
|
this.AttemptLogin() // Also updates the user badge
|
||||||
} else {
|
} else {
|
||||||
console.log("Not authenticated");
|
console.log("Not authenticated")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,25 +145,25 @@ export class OsmConnection {
|
||||||
prefix?: string
|
prefix?: string
|
||||||
}
|
}
|
||||||
): UIEventSource<string> {
|
): UIEventSource<string> {
|
||||||
return this.preferencesHandler.GetPreference(key, defaultValue, options);
|
return this.preferencesHandler.GetPreference(key, defaultValue, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||||
return this.preferencesHandler.GetLongPreference(key, prefix);
|
return this.preferencesHandler.GetLongPreference(key, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
public OnLoggedIn(action: (userDetails: UserDetails) => void) {
|
public OnLoggedIn(action: (userDetails: UserDetails) => void) {
|
||||||
this._onLoggedIn.push(action);
|
this._onLoggedIn.push(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
public LogOut() {
|
public LogOut() {
|
||||||
this.auth.logout();
|
this.auth.logout()
|
||||||
this.userDetails.data.loggedIn = false;
|
this.userDetails.data.loggedIn = false
|
||||||
this.userDetails.data.csCount = 0;
|
this.userDetails.data.csCount = 0
|
||||||
this.userDetails.data.name = "";
|
this.userDetails.data.name = ""
|
||||||
this.userDetails.ping();
|
this.userDetails.ping()
|
||||||
console.log("Logged out");
|
console.log("Logged out")
|
||||||
this.loadingStatus.setData("not-attempted");
|
this.loadingStatus.setData("not-attempted")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -183,95 +172,95 @@ export class OsmConnection {
|
||||||
* new OsmConnection().Backend() // => "https://www.openstreetmap.org"
|
* new OsmConnection().Backend() // => "https://www.openstreetmap.org"
|
||||||
*/
|
*/
|
||||||
public Backend(): string {
|
public Backend(): string {
|
||||||
return this._oauth_config.url;
|
return this._oauth_config.url
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttemptLogin() {
|
public AttemptLogin() {
|
||||||
this.UpdateCapabilities();
|
this.UpdateCapabilities()
|
||||||
this.loadingStatus.setData("loading");
|
this.loadingStatus.setData("loading")
|
||||||
if (this.fakeUser) {
|
if (this.fakeUser) {
|
||||||
this.loadingStatus.setData("logged-in");
|
this.loadingStatus.setData("logged-in")
|
||||||
console.log("AttemptLogin called, but ignored as fakeUser is set");
|
console.log("AttemptLogin called, but ignored as fakeUser is set")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const self = this;
|
const self = this
|
||||||
console.log("Trying to log in...");
|
console.log("Trying to log in...")
|
||||||
this.updateAuthObject();
|
this.updateAuthObject()
|
||||||
LocalStorageSource.Get("location_before_login").setData(
|
LocalStorageSource.Get("location_before_login").setData(
|
||||||
Utils.runningFromConsole ? undefined : window.location.href
|
Utils.runningFromConsole ? undefined : window.location.href
|
||||||
);
|
)
|
||||||
this.auth.xhr(
|
this.auth.xhr(
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/api/0.6/user/details"
|
path: "/api/0.6/user/details",
|
||||||
},
|
},
|
||||||
function (err, details) {
|
function (err, details) {
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
console.log(err);
|
console.log(err)
|
||||||
self.loadingStatus.setData("error");
|
self.loadingStatus.setData("error")
|
||||||
if (err.status == 401) {
|
if (err.status == 401) {
|
||||||
console.log("Clearing tokens...");
|
console.log("Clearing tokens...")
|
||||||
// Not authorized - our token probably got revoked
|
// Not authorized - our token probably got revoked
|
||||||
self.auth.logout();
|
self.auth.logout()
|
||||||
self.LogOut();
|
self.LogOut()
|
||||||
}
|
}
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (details == null) {
|
if (details == null) {
|
||||||
self.loadingStatus.setData("error");
|
self.loadingStatus.setData("error")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.CheckForMessagesContinuously();
|
self.CheckForMessagesContinuously()
|
||||||
|
|
||||||
// details is an XML DOM of user details
|
// details is an XML DOM of user details
|
||||||
let userInfo = details.getElementsByTagName("user")[0];
|
let userInfo = details.getElementsByTagName("user")[0]
|
||||||
|
|
||||||
let data = self.userDetails.data;
|
let data = self.userDetails.data
|
||||||
data.loggedIn = true;
|
data.loggedIn = true
|
||||||
console.log("Login completed, userinfo is ", userInfo);
|
console.log("Login completed, userinfo is ", userInfo)
|
||||||
data.name = userInfo.getAttribute("display_name");
|
data.name = userInfo.getAttribute("display_name")
|
||||||
data.account_created = userInfo.getAttribute("account_created");
|
data.account_created = userInfo.getAttribute("account_created")
|
||||||
data.uid = Number(userInfo.getAttribute("id"));
|
data.uid = Number(userInfo.getAttribute("id"))
|
||||||
data.csCount = Number.parseInt(
|
data.csCount = Number.parseInt(
|
||||||
userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? 0
|
userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? 0
|
||||||
);
|
)
|
||||||
data.tracesCount = Number.parseInt(
|
data.tracesCount = Number.parseInt(
|
||||||
userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? 0
|
userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? 0
|
||||||
);
|
)
|
||||||
|
|
||||||
data.img = undefined;
|
data.img = undefined
|
||||||
const imgEl = userInfo.getElementsByTagName("img");
|
const imgEl = userInfo.getElementsByTagName("img")
|
||||||
if (imgEl !== undefined && imgEl[0] !== undefined) {
|
if (imgEl !== undefined && imgEl[0] !== undefined) {
|
||||||
data.img = imgEl[0].getAttribute("href");
|
data.img = imgEl[0].getAttribute("href")
|
||||||
}
|
}
|
||||||
|
|
||||||
const description = userInfo.getElementsByTagName("description");
|
const description = userInfo.getElementsByTagName("description")
|
||||||
if (description !== undefined && description[0] !== undefined) {
|
if (description !== undefined && description[0] !== undefined) {
|
||||||
data.description = description[0]?.innerHTML;
|
data.description = description[0]?.innerHTML
|
||||||
}
|
}
|
||||||
const homeEl = userInfo.getElementsByTagName("home");
|
const homeEl = userInfo.getElementsByTagName("home")
|
||||||
if (homeEl !== undefined && homeEl[0] !== undefined) {
|
if (homeEl !== undefined && homeEl[0] !== undefined) {
|
||||||
const lat = parseFloat(homeEl[0].getAttribute("lat"));
|
const lat = parseFloat(homeEl[0].getAttribute("lat"))
|
||||||
const lon = parseFloat(homeEl[0].getAttribute("lon"));
|
const lon = parseFloat(homeEl[0].getAttribute("lon"))
|
||||||
data.home = { lat: lat, lon: lon };
|
data.home = { lat: lat, lon: lon }
|
||||||
}
|
}
|
||||||
|
|
||||||
self.loadingStatus.setData("logged-in");
|
self.loadingStatus.setData("logged-in")
|
||||||
const messages = userInfo
|
const messages = userInfo
|
||||||
.getElementsByTagName("messages")[0]
|
.getElementsByTagName("messages")[0]
|
||||||
.getElementsByTagName("received")[0];
|
.getElementsByTagName("received")[0]
|
||||||
data.unreadMessages = parseInt(messages.getAttribute("unread"));
|
data.unreadMessages = parseInt(messages.getAttribute("unread"))
|
||||||
data.totalMessages = parseInt(messages.getAttribute("count"));
|
data.totalMessages = parseInt(messages.getAttribute("count"))
|
||||||
|
|
||||||
self.userDetails.ping();
|
self.userDetails.ping()
|
||||||
for (const action of self._onLoggedIn) {
|
for (const action of self._onLoggedIn) {
|
||||||
action(self.userDetails.data);
|
action(self.userDetails.data)
|
||||||
}
|
}
|
||||||
self._onLoggedIn = [];
|
self._onLoggedIn = []
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -290,20 +279,20 @@ export class OsmConnection {
|
||||||
{
|
{
|
||||||
method,
|
method,
|
||||||
options: {
|
options: {
|
||||||
header
|
header,
|
||||||
},
|
},
|
||||||
content,
|
content,
|
||||||
path: `/api/0.6/${path}`
|
path: `/api/0.6/${path}`,
|
||||||
},
|
},
|
||||||
function (err, response) {
|
function (err, response) {
|
||||||
if (err !== null) {
|
if (err !== null) {
|
||||||
error(err);
|
error(err)
|
||||||
} else {
|
} else {
|
||||||
ok(response);
|
ok(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public async post(
|
public async post(
|
||||||
|
@ -311,7 +300,7 @@ export class OsmConnection {
|
||||||
content?: string,
|
content?: string,
|
||||||
header?: Record<string, string | number>
|
header?: Record<string, string | number>
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return await this.interact(path, "POST", header, content);
|
return await this.interact(path, "POST", header, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async put(
|
public async put(
|
||||||
|
@ -319,60 +308,60 @@ export class OsmConnection {
|
||||||
content?: string,
|
content?: string,
|
||||||
header?: Record<string, string | number>
|
header?: Record<string, string | number>
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return await this.interact(path, "PUT", header, content);
|
return await this.interact(path, "PUT", header, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(path: string, header?: Record<string, string | number>): Promise<any> {
|
public async get(path: string, header?: Record<string, string | number>): Promise<any> {
|
||||||
return await this.interact(path, "GET", header);
|
return await this.interact(path, "GET", header)
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeNote(id: number | string, text?: string): Promise<void> {
|
public closeNote(id: number | string, text?: string): Promise<void> {
|
||||||
let textSuffix = "";
|
let textSuffix = ""
|
||||||
if ((text ?? "") !== "") {
|
if ((text ?? "") !== "") {
|
||||||
textSuffix = "?text=" + encodeURIComponent(text);
|
textSuffix = "?text=" + encodeURIComponent(text)
|
||||||
}
|
}
|
||||||
if (this._dryRun.data) {
|
if (this._dryRun.data) {
|
||||||
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text);
|
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
|
||||||
return new Promise((ok) => {
|
return new Promise((ok) => {
|
||||||
ok();
|
ok()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
return this.post(`notes/${id}/close${textSuffix}`);
|
return this.post(`notes/${id}/close${textSuffix}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
public reopenNote(id: number | string, text?: string): Promise<void> {
|
public reopenNote(id: number | string, text?: string): Promise<void> {
|
||||||
if (this._dryRun.data) {
|
if (this._dryRun.data) {
|
||||||
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text);
|
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
|
||||||
return new Promise((ok) => {
|
return new Promise((ok) => {
|
||||||
ok();
|
ok()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
let textSuffix = "";
|
let textSuffix = ""
|
||||||
if ((text ?? "") !== "") {
|
if ((text ?? "") !== "") {
|
||||||
textSuffix = "?text=" + encodeURIComponent(text);
|
textSuffix = "?text=" + encodeURIComponent(text)
|
||||||
}
|
}
|
||||||
return this.post(`notes/${id}/reopen${textSuffix}`);
|
return this.post(`notes/${id}/reopen${textSuffix}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openNote(lat: number, lon: number, text: string): Promise<{ id: number }> {
|
public async openNote(lat: number, lon: number, text: string): Promise<{ id: number }> {
|
||||||
if (this._dryRun.data) {
|
if (this._dryRun.data) {
|
||||||
console.warn("Dryrun enabled - not actually opening note with text ", text);
|
console.warn("Dryrun enabled - not actually opening note with text ", text)
|
||||||
return new Promise<{ id: number }>((ok) => {
|
return new Promise<{ id: number }>((ok) => {
|
||||||
window.setTimeout(
|
window.setTimeout(
|
||||||
() => ok({ id: Math.floor(Math.random() * 1000) }),
|
() => ok({ id: Math.floor(Math.random() * 1000) }),
|
||||||
Math.random() * 5000
|
Math.random() * 5000
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
// Lat and lon must be strings for the API to accept it
|
// Lat and lon must be strings for the API to accept it
|
||||||
const content = `lat=${lat}&lon=${lon}&text=${encodeURIComponent(text)}`
|
const content = `lat=${lat}&lon=${lon}&text=${encodeURIComponent(text)}`
|
||||||
const response = await this.post("notes.json", content, {
|
const response = await this.post("notes.json", content, {
|
||||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
});
|
})
|
||||||
const parsed = JSON.parse(response);
|
const parsed = JSON.parse(response)
|
||||||
const id = parsed.properties;
|
const id = parsed.properties
|
||||||
console.log("OPENED NOTE", id);
|
console.log("OPENED NOTE", id)
|
||||||
return id;
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
public async uploadGpxTrack(
|
public async uploadGpxTrack(
|
||||||
|
@ -390,61 +379,61 @@ export class OsmConnection {
|
||||||
}
|
}
|
||||||
): Promise<{ id: number }> {
|
): Promise<{ id: number }> {
|
||||||
if (this._dryRun.data) {
|
if (this._dryRun.data) {
|
||||||
console.warn("Dryrun enabled - not actually uploading GPX ", gpx);
|
console.warn("Dryrun enabled - not actually uploading GPX ", gpx)
|
||||||
return new Promise<{ id: number }>((ok, error) => {
|
return new Promise<{ id: number }>((ok, error) => {
|
||||||
window.setTimeout(
|
window.setTimeout(
|
||||||
() => ok({ id: Math.floor(Math.random() * 1000) }),
|
() => ok({ id: Math.floor(Math.random() * 1000) }),
|
||||||
Math.random() * 5000
|
Math.random() * 5000
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const contents = {
|
const contents = {
|
||||||
file: gpx,
|
file: gpx,
|
||||||
description: options.description ?? "",
|
description: options.description ?? "",
|
||||||
tags: options.labels?.join(",") ?? "",
|
tags: options.labels?.join(",") ?? "",
|
||||||
visibility: options.visibility
|
visibility: options.visibility,
|
||||||
};
|
}
|
||||||
|
|
||||||
const extras = {
|
const extras = {
|
||||||
file:
|
file:
|
||||||
"; filename=\"" +
|
'; filename="' +
|
||||||
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
|
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
|
||||||
"\"\r\nContent-Type: application/gpx+xml"
|
'"\r\nContent-Type: application/gpx+xml',
|
||||||
};
|
}
|
||||||
|
|
||||||
const boundary = "987654";
|
const boundary = "987654"
|
||||||
|
|
||||||
let body = "";
|
let body = ""
|
||||||
for (const key in contents) {
|
for (const key in contents) {
|
||||||
body += "--" + boundary + "\r\n";
|
body += "--" + boundary + "\r\n"
|
||||||
body += "Content-Disposition: form-data; name=\"" + key + "\"";
|
body += 'Content-Disposition: form-data; name="' + key + '"'
|
||||||
if (extras[key] !== undefined) {
|
if (extras[key] !== undefined) {
|
||||||
body += extras[key];
|
body += extras[key]
|
||||||
}
|
}
|
||||||
body += "\r\n\r\n";
|
body += "\r\n\r\n"
|
||||||
body += contents[key] + "\r\n";
|
body += contents[key] + "\r\n"
|
||||||
}
|
}
|
||||||
body += "--" + boundary + "--\r\n";
|
body += "--" + boundary + "--\r\n"
|
||||||
|
|
||||||
const response = await this.post("gpx/create", body, {
|
const response = await this.post("gpx/create", body, {
|
||||||
"Content-Type": "multipart/form-data; boundary=" + boundary,
|
"Content-Type": "multipart/form-data; boundary=" + boundary,
|
||||||
"Content-Length": body.length
|
"Content-Length": body.length,
|
||||||
});
|
})
|
||||||
const parsed = JSON.parse(response);
|
const parsed = JSON.parse(response)
|
||||||
console.log("Uploaded GPX track", parsed);
|
console.log("Uploaded GPX track", parsed)
|
||||||
return { id: parsed };
|
return { id: parsed }
|
||||||
}
|
}
|
||||||
|
|
||||||
public addCommentToNote(id: number | string, text: string): Promise<void> {
|
public addCommentToNote(id: number | string, text: string): Promise<void> {
|
||||||
if (this._dryRun.data) {
|
if (this._dryRun.data) {
|
||||||
console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id);
|
console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id)
|
||||||
return new Promise((ok) => {
|
return new Promise((ok) => {
|
||||||
ok();
|
ok()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
if ((text ?? "") === "") {
|
if ((text ?? "") === "") {
|
||||||
throw "Invalid text!";
|
throw "Invalid text!"
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((ok, error) => {
|
return new Promise((ok, error) => {
|
||||||
|
@ -452,17 +441,17 @@ export class OsmConnection {
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
||||||
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
|
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`,
|
||||||
},
|
},
|
||||||
function (err, _) {
|
function (err, _) {
|
||||||
if (err !== null) {
|
if (err !== null) {
|
||||||
error(err);
|
error(err)
|
||||||
} else {
|
} else {
|
||||||
ok();
|
ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -471,31 +460,31 @@ export class OsmConnection {
|
||||||
public finishLogin(callback: (previousURL: string) => void) {
|
public finishLogin(callback: (previousURL: string) => void) {
|
||||||
this.auth.authenticate(function () {
|
this.auth.authenticate(function () {
|
||||||
// Fully authed at this point
|
// Fully authed at this point
|
||||||
console.log("Authentication successful!");
|
console.log("Authentication successful!")
|
||||||
const previousLocation = LocalStorageSource.Get("location_before_login");
|
const previousLocation = LocalStorageSource.Get("location_before_login")
|
||||||
callback(previousLocation.data);
|
callback(previousLocation.data)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateAuthObject() {
|
private updateAuthObject() {
|
||||||
let pwaStandAloneMode = false;
|
let pwaStandAloneMode = false
|
||||||
try {
|
try {
|
||||||
if (Utils.runningFromConsole) {
|
if (Utils.runningFromConsole) {
|
||||||
pwaStandAloneMode = true;
|
pwaStandAloneMode = true
|
||||||
} else if (
|
} else if (
|
||||||
window.matchMedia("(display-mode: standalone)").matches ||
|
window.matchMedia("(display-mode: standalone)").matches ||
|
||||||
window.matchMedia("(display-mode: fullscreen)").matches
|
window.matchMedia("(display-mode: fullscreen)").matches
|
||||||
) {
|
) {
|
||||||
pwaStandAloneMode = true;
|
pwaStandAloneMode = true
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Detecting standalone mode failed",
|
"Detecting standalone mode failed",
|
||||||
e,
|
e,
|
||||||
". Assuming in browser and not worrying furhter"
|
". Assuming in browser and not worrying furhter"
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage;
|
const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage
|
||||||
|
|
||||||
// In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
|
// In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
|
||||||
// Same for an iframe...
|
// Same for an iframe...
|
||||||
|
@ -508,46 +497,46 @@ export class OsmConnection {
|
||||||
? "https://mapcomplete.org/land.html"
|
? "https://mapcomplete.org/land.html"
|
||||||
: window.location.protocol + "//" + window.location.host + "/land.html",
|
: window.location.protocol + "//" + window.location.host + "/land.html",
|
||||||
singlepage: !standalone,
|
singlepage: !standalone,
|
||||||
auto: true
|
auto: true,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private CheckForMessagesContinuously() {
|
private CheckForMessagesContinuously() {
|
||||||
const self = this;
|
const self = this
|
||||||
if (this.isChecking) {
|
if (this.isChecking) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
this.isChecking = true;
|
this.isChecking = true
|
||||||
Stores.Chronic(5 * 60 * 1000).addCallback((_) => {
|
Stores.Chronic(5 * 60 * 1000).addCallback((_) => {
|
||||||
if (self.isLoggedIn.data) {
|
if (self.isLoggedIn.data) {
|
||||||
console.log("Checking for messages");
|
console.log("Checking for messages")
|
||||||
self.AttemptLogin();
|
self.AttemptLogin()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private UpdateCapabilities(): void {
|
private UpdateCapabilities(): void {
|
||||||
const self = this;
|
const self = this
|
||||||
this.FetchCapabilities().then(({ api, gpx }) => {
|
this.FetchCapabilities().then(({ api, gpx }) => {
|
||||||
self.apiIsOnline.setData(api);
|
self.apiIsOnline.setData(api)
|
||||||
self.gpxServiceIsOnline.setData(gpx);
|
self.gpxServiceIsOnline.setData(gpx)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> {
|
private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> {
|
||||||
if (Utils.runningFromConsole) {
|
if (Utils.runningFromConsole) {
|
||||||
return { api: "online", gpx: "online" };
|
return { api: "online", gpx: "online" }
|
||||||
}
|
}
|
||||||
const result = await Utils.downloadAdvanced(this.Backend() + "/api/0.6/capabilities");
|
const result = await Utils.downloadAdvanced(this.Backend() + "/api/0.6/capabilities")
|
||||||
if (result["content"] === undefined) {
|
if (result["content"] === undefined) {
|
||||||
console.log("Something went wrong:", result);
|
console.log("Something went wrong:", result)
|
||||||
return { api: "unreachable", gpx: "unreachable" };
|
return { api: "unreachable", gpx: "unreachable" }
|
||||||
}
|
}
|
||||||
const xmlRaw = result["content"];
|
const xmlRaw = result["content"]
|
||||||
const parsed = new DOMParser().parseFromString(xmlRaw, "text/xml");
|
const parsed = new DOMParser().parseFromString(xmlRaw, "text/xml")
|
||||||
const statusEl = parsed.getElementsByTagName("status")[0];
|
const statusEl = parsed.getElementsByTagName("status")[0]
|
||||||
const api = <OsmServiceState>statusEl.getAttribute("api");
|
const api = <OsmServiceState>statusEl.getAttribute("api")
|
||||||
const gpx = <OsmServiceState>statusEl.getAttribute("gpx");
|
const gpx = <OsmServiceState>statusEl.getAttribute("gpx")
|
||||||
return { api, gpx };
|
return { api, gpx }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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([
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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>>,
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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`). "
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
32
src/UI/RemoveOtherLanguages.ts
Normal file
32
src/UI/RemoveOtherLanguages.ts
Normal 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"))
|
|
@ -1,104 +1,108 @@
|
||||||
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,
|
||||||
|
@ -106,7 +110,7 @@ export interface SpecialVisualization {
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig
|
||||||
): BaseUIElement;
|
): BaseUIElement
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RenderingSpecification =
|
export type RenderingSpecification =
|
||||||
|
|
|
@ -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>>,
|
||||||
|
|
|
@ -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
|
@ -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 {
|
||||||
|
|
55
theme.html
55
theme.html
|
@ -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>
|
<script async src="./src/InstallServiceWorker.ts" type="module"></script>
|
||||||
|
<script defer src="./src/index.ts" type="module"></script>
|
||||||
let lang = ((navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage || 'en').substr(0, 2);
|
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
|
||||||
|
|
||||||
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>
|
||||||
|
|
Loading…
Reference in a new issue