Add module to fetch data (via a proxy) from the website with jsonld

This commit is contained in:
Pieter Vander Vennet 2024-02-26 02:24:46 +01:00
parent 1b06eee15b
commit 352414b29d
17 changed files with 388 additions and 351 deletions

View file

@ -90,11 +90,8 @@
"id": "show-data-velopark", "id": "show-data-velopark",
"render": { "render": {
"special": { "special": {
"type": "compare_data", "type": "linked_data_from_website",
"url": "ref:velopark", "key": "ref:velopark"
"host": "https://data.velopark.be",
"postprocessing": "velopark",
"readonly": "yes"
} }
} }
}, },
@ -338,10 +335,8 @@
}, },
"render": { "render": {
"special": { "special": {
"type": "compare_data", "type": "linked_data_from_website",
"url": "ref:velopark", "key": "ref:velopark"
"host": "https://data.velopark.be",
"postprocessing": "velopark"
} }
} }
} }

View file

@ -94,7 +94,9 @@
"weblate-fix-heavy": "git fetch weblate-hosted-layers; git fetch weblate-hosted-core; git merge weblate-hosted-layers/master weblate-hosted-core/master ", "weblate-fix-heavy": "git fetch weblate-hosted-layers; git fetch weblate-hosted-core; git merge weblate-hosted-layers/master weblate-hosted-core/master ",
"housekeeping": "git pull && npx update-browserslist-db@latest && npm run weblate-fix-heavy && npm run generate && npm run generate:docs && npm run generate:contributor-list && vite-node scripts/fetchLanguages.ts && npm run format && git add assets/ langs/ Docs/ **/*.ts Docs/* src/* && git commit -m 'chore: automated housekeeping...'", "housekeeping": "git pull && npx update-browserslist-db@latest && npm run weblate-fix-heavy && npm run generate && npm run generate:docs && npm run generate:contributor-list && vite-node scripts/fetchLanguages.ts && npm run format && git add assets/ langs/ Docs/ **/*.ts Docs/* src/* && git commit -m 'chore: automated housekeeping...'",
"reuse-compliance": "reuse lint", "reuse-compliance": "reuse lint",
"backup:images": "vite-node scripts/generateImageAnalysis.ts -- ~/data/imgur-image-backup/" "backup:images": "vite-node scripts/generateImageAnalysis.ts -- ~/data/imgur-image-backup/",
"dloadVelopark": "vite-node scripts/velopark/veloParkToGeojson.ts ",
"scrapeWebsites": "vite-node scripts/importscripts/compareWebsiteData.ts -- ~/Downloads/ShopsWithWebsiteNodes.csv ~/data/scraped_websites/\n"
}, },
"keywords": [ "keywords": [
"OpenStreetMap", "OpenStreetMap",

View file

@ -148,7 +148,16 @@ export default class ScriptUtils {
const data = await ScriptUtils.Download(url, headers) const data = await ScriptUtils.Download(url, headers)
return JSON.parse(data["content"]) return JSON.parse(data["content"])
} }
public static async DownloadFetch(
url: string,
headers?: any
): Promise<{ content: string } | { redirect: string }> {
console.log("Fetching", url)
const req = await fetch(url, {headers})
const data= await req.text()
console.log("Fetched", url,data)
return {content: data}
}
public static Download( public static Download(
url: string, url: string,
headers?: any headers?: any

View file

@ -0,0 +1,80 @@
import fs from "fs"
// import readline from "readline"
import Script from "../Script"
import LinkedDataLoader from "../../src/Logic/Web/LinkedDataLoader"
import UrlValidator from "../../src/UI/InputElement/Validators/UrlValidator"
// vite-node scripts/importscripts/compareWebsiteData.ts -- ~/Downloads/ShopsWithWebsiteNodes.csv ~/data/scraped_websites/
/*
class CompareWebsiteData extends Script {
constructor() {
super("Given a csv file with 'id', 'tags' and 'website', attempts to fetch jsonld and compares the attributes. Usage: csv-file datadir")
}
private readonly urlFormatter = new UrlValidator()
async getWithCache(cachedir : string, url: string): Promise<any>{
const filename= cachedir+"/"+encodeURIComponent(url)
if(fs.existsSync(filename)){
return JSON.parse(fs.readFileSync(filename, "utf-8"))
}
const jsonLd = await LinkedDataLoader.fetchJsonLdWithProxy(url)
console.log("Got:", jsonLd)
fs.writeFileSync(filename, JSON.stringify(jsonLd))
return jsonLd
}
async handleEntry(line: string, cachedir: string, targetfile: string) : Promise<boolean>{
const id = JSON.parse(line.split(",")[0])
let tags = line.substring(line.indexOf("{") - 1)
tags = tags.substring(1, tags.length - 1)
tags = tags.replace(/""/g, "\"")
const data = JSON.parse(tags)
const website = data.website //this.urlFormatter.reformat(data.website)
if(!website.startsWith("https://stores.delhaize.be")){
return false
}
console.log(website)
const jsonld = await this.getWithCache(cachedir, website)
console.log(jsonld)
if(Object.keys(jsonld).length === 0){
return false
}
const diff = LinkedDataLoader.removeDuplicateData(jsonld, data)
fs.appendFileSync(targetfile, id +", "+ JSON.stringify(diff)+"\n")
return true
}
async main(args: string[]): Promise<void> {
if (args.length < 2) {
throw "Not enough arguments"
}
const readInterface = readline.createInterface({
input: fs.createReadStream(args[0]),
})
let handled = 0
let diffed = 0
const targetfile = "diff.csv"
fs.writeFileSync(targetfile, "id, diff-json\n")
for await (const line of readInterface) {
try {
if(line.startsWith("\"id\"")){
continue
}
const madeComparison = await this.handleEntry(line, args[1], targetfile)
handled ++
diffed = diffed + (madeComparison ? 1 : 0)
if(handled % 1000 == 0){
// console.log("Handled ",handled," got ",diffed,"diff results")
}
} catch (e) {
// console.error(e)
}
}
}
}
new CompareWebsiteData().run()
*/

0
scripts/scrapeOsm.ts Normal file
View file

View file

@ -15,6 +15,7 @@ class ServerLdScrape extends Script {
mimetype: "application/ld+json", mimetype: "application/ld+json",
async handle(content, searchParams: URLSearchParams) { async handle(content, searchParams: URLSearchParams) {
const url = searchParams.get("url") const url = searchParams.get("url")
console.log("Fetching", url)
if (cache[url]) { if (cache[url]) {
return JSON.stringify(cache[url]) return JSON.stringify(cache[url])
} }

View file

@ -1,39 +1,42 @@
import Script from "../Script" import Script from "../Script"
import { Utils } from "../../src/Utils"
import VeloparkLoader, { VeloparkData } from "../../src/Logic/Web/VeloparkLoader"
import fs from "fs" import fs from "fs"
import { Overpass } from "../../src/Logic/Osm/Overpass" import { Overpass } from "../../src/Logic/Osm/Overpass"
import { RegexTag } from "../../src/Logic/Tags/RegexTag" import { RegexTag } from "../../src/Logic/Tags/RegexTag"
import Constants from "../../src/Models/Constants" import Constants from "../../src/Models/Constants"
import { ImmutableStore } from "../../src/Logic/UIEventSource" import { ImmutableStore } from "../../src/Logic/UIEventSource"
import { BBox } from "../../src/Logic/BBox" import { BBox } from "../../src/Logic/BBox"
import LinkedDataLoader from "../../src/Logic/Web/LinkedDataLoader"
class VeloParkToGeojson extends Script { class VeloParkToGeojson extends Script {
constructor() { constructor() {
super( super(
"Downloads the latest Velopark data and converts it to a geojson, which will be saved at the current directory" "Downloads the latest Velopark data and converts it to a geojson, which will be saved at the current directory",
) )
} }
exportTo(filename: string, features) { exportTo(filename: string, features) {
fs.writeFileSync( features = features.slice(0,25) // TODO REMOVE
filename + "_" + new Date().toISOString() + ".geojson", const file = filename + "_" + /*new Date().toISOString() + */".geojson"
fs.writeFileSync(file,
JSON.stringify( JSON.stringify(
{ {
type: "FeatureCollection", type: "FeatureCollection",
"#":"Only 25 features are shown!", // TODO REMOVE
features, features,
}, },
null, null,
" " " ",
) ),
) )
console.log("Written",file)
} }
async main(args: string[]): Promise<void> { async main(args: string[]): Promise<void> {
console.log("Downloading velopark data") console.log("Downloading velopark data")
// Download data for NIS-code 1000. 1000 means: all of belgium // Download data for NIS-code 1000. 1000 means: all of belgium
const url = "https://www.velopark.be/api/parkings/1000" const url = "https://www.velopark.be/api/parkings/1000"
const data = <VeloparkData[]>await Utils.downloadJson(url) const allVelopark = await LinkedDataLoader.fetchJsonLd(url, { country: "be" })
this.exportTo("velopark_all", allVelopark)
const bboxBelgium = new BBox([ const bboxBelgium = new BBox([
[2.51357303225, 49.5294835476], [2.51357303225, 49.5294835476],
@ -44,15 +47,13 @@ class VeloParkToGeojson extends Script {
[], [],
Constants.defaultOverpassUrls[0], Constants.defaultOverpassUrls[0],
new ImmutableStore(60 * 5), new ImmutableStore(60 * 5),
false false,
) )
const alreadyLinkedFeatures = await alreadyLinkedQuery.queryGeoJson(bboxBelgium) const alreadyLinkedFeatures = await alreadyLinkedQuery.queryGeoJson(bboxBelgium)
const seenIds = new Set<string>( const seenIds = new Set<string>(
alreadyLinkedFeatures[0].features.map((f) => f.properties["ref:velopark"]) alreadyLinkedFeatures[0].features.map((f) => f.properties["ref:velopark"]),
) )
console.log("OpenStreetMap contains", seenIds.size, "bicycle parkings with a velopark ref") console.log("OpenStreetMap contains", seenIds.size, "bicycle parkings with a velopark ref")
const allVelopark = data.map((f) => VeloparkLoader.convert(f))
this.exportTo("velopark_all", allVelopark)
const features = allVelopark.filter((f) => !seenIds.has(f.properties["ref:velopark"])) const features = allVelopark.filter((f) => !seenIds.has(f.properties["ref:velopark"]))

View file

@ -237,8 +237,11 @@ export abstract class Store<T> implements Readable<T> {
public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>): Store<X> { public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>): Store<X> {
return this.bind((t) => { return this.bind((t) => {
if (t === undefined || t === null) { if(t=== null){
return <undefined | null>t return null
}
if (t === undefined ) {
return undefined
} }
return f(<Exclude<T, undefined | null>>t) return f(<Exclude<T, undefined | null>>t)
}) })

View file

@ -6,7 +6,11 @@ import PhoneValidator from "../../UI/InputElement/Validators/PhoneValidator"
import EmailValidator from "../../UI/InputElement/Validators/EmailValidator" import EmailValidator from "../../UI/InputElement/Validators/EmailValidator"
import { Validator } from "../../UI/InputElement/Validator" import { Validator } from "../../UI/InputElement/Validator"
import UrlValidator from "../../UI/InputElement/Validators/UrlValidator" import UrlValidator from "../../UI/InputElement/Validators/UrlValidator"
import Constants from "../../Models/Constants"
interface JsonLdLoaderOptions {
country?: string
}
export default class LinkedDataLoader { export default class LinkedDataLoader {
private static readonly COMPACTING_CONTEXT = { private static readonly COMPACTING_CONTEXT = {
name: "http://schema.org/name", name: "http://schema.org/name",
@ -43,6 +47,10 @@ export default class LinkedDataLoader {
"http://schema.org/contactPoint", "http://schema.org/contactPoint",
] ]
private static ignoreTypes = [
"Breadcrumblist"
]
static async geoToGeometry(geo): Promise<Geometry> { static async geoToGeometry(geo): Promise<Geometry> {
const context = { const context = {
lat: { lat: {
@ -102,15 +110,30 @@ export default class LinkedDataLoader {
return OH.ToString(OH.MergeTimes(allRules)) return OH.ToString(OH.MergeTimes(allRules))
} }
static async fetchJsonLd(url: string, country?: string): Promise<Record<string, any>> { static async fetchJsonLdWithProxy(url: string, options?: JsonLdLoaderOptions): Promise<any> {
const proxy = "http://127.0.0.1:2346/extractgraph" // "https://cache.mapcomplete.org/extractgraph" const urlWithProxy = Constants.linkedDataProxy.replace("{url}", encodeURIComponent(url))
const data = await Utils.downloadJson(`${proxy}?url=${url}`) return await this.fetchJsonLd(urlWithProxy, options)
const compacted = await jsonld.compact(data, LinkedDataLoader.COMPACTING_CONTEXT) }
/**
*
*
* {
* "content": "{\"@context\":\"http://schema.org\",\"@type\":\"LocalBusiness\",\"@id\":\"http://stores.delhaize.be/nl/ad-delhaize-munsterbilzen\",\"name\":\"AD Delhaize Munsterbilzen\",\"url\":\"http://stores.delhaize.be/nl/ad-delhaize-munsterbilzen\",\"logo\":\"https://stores.delhaize.be/build/images/web/shop/delhaize-be/favicon.ico\",\"image\":\"http://stores.delhaize.be/image/mobilosoft-testing?apiPath=rehab/delhaize-be/images/location/ad%20delhaize%20image%20ge%CC%81ne%CC%81rale%20%281%29%201652787176865&imageSize=h_500\",\"email\":\"\",\"telephone\":\"+3289413520\",\"address\":{\"@type\":\"PostalAddress\",\"streetAddress\":\"Waterstraat, 18\",\"addressLocality\":\"Bilzen\",\"postalCode\":\"3740\",\"addressCountry\":\"BE\"},\"geo\":{\"@type\":\"GeoCoordinates\",\"latitude\":50.8906898,\"longitude\":5.5260586},\"openingHoursSpecification\":[{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Tuesday\",\"opens\":\"08:00\",\"closes\":\"18:30\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Wednesday\",\"opens\":\"08:00\",\"closes\":\"18:30\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Thursday\",\"opens\":\"08:00\",\"closes\":\"18:30\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Friday\",\"opens\":\"08:00\",\"closes\":\"18:30\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Saturday\",\"opens\":\"08:00\",\"closes\":\"18:30\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Sunday\",\"opens\":\"08:00\",\"closes\":\"12:00\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Monday\",\"opens\":\"12:00\",\"closes\":\"18:30\"}],\"@base\":\"https://stores.delhaize.be/nl/ad-delhaize-munsterbilzen\"}"
* }
*/
private static async compact(data: any, options?: JsonLdLoaderOptions): Promise<any>{
console.log("Compacting",data)
if(Array.isArray(data)) {
return await Promise.all(data.map(d => LinkedDataLoader.compact(d)))
}
const country = options?.country
const compacted = await jsonld.compact(data, <any> LinkedDataLoader.COMPACTING_CONTEXT)
compacted["opening_hours"] = await LinkedDataLoader.ohToOsmFormat( compacted["opening_hours"] = await LinkedDataLoader.ohToOsmFormat(
compacted["opening_hours"] compacted["opening_hours"]
) )
if (compacted["openingHours"]) { if (compacted["openingHours"]) {
const ohspec: string[] = compacted["openingHours"] const ohspec: string[] = <any> compacted["openingHours"]
compacted["opening_hours"] = OH.simplify( compacted["opening_hours"] = OH.simplify(
ohspec.map((r) => LinkedDataLoader.ohStringToOsmFormat(r)).join("; ") ohspec.map((r) => LinkedDataLoader.ohStringToOsmFormat(r)).join("; ")
) )
@ -138,5 +161,39 @@ export default class LinkedDataLoader {
} }
} }
return <any>compacted return <any>compacted
}
static async fetchJsonLd(url: string, options?: JsonLdLoaderOptions): Promise<any> {
const data = await Utils.downloadJson(url)
return await LinkedDataLoader.compact(data, options)
}
/**
* Only returns different items
* @param externalData
* @param currentData
*/
static removeDuplicateData(externalData: Record<string, string>, currentData: Record<string, string>) : Record<string, string>{
const d = { ...externalData }
delete d["@context"]
for (const k in d) {
const v = currentData[k]
if (!v) {
continue
}
if (k === "opening_hours") {
const oh = [].concat(...v.split(";").map(r => OH.ParseRule(r) ?? []))
const merged = OH.ToString(OH.MergeTimes(oh ?? []))
if (merged === d[k]) {
delete d[k]
continue
}
}
if (v === d[k]) {
delete d[k]
}
delete d.geo
}
return d
} }
} }

View file

@ -33,7 +33,7 @@
{ {
theme: state.layout.id, theme: state.layout.id,
changeType: "import", changeType: "import",
} },
) )
await state.changes.applyChanges(await change.CreateChangeDescriptions()) await state.changes.applyChanges(await change.CreateChangeDescriptions())
currentStep = "done" currentStep = "done"
@ -43,6 +43,9 @@
<tr> <tr>
<td><b>{key}</b></td> <td><b>{key}</b></td>
{#if $tags[key]}
{$tags[key]}
{/if}
<td> <td>
{#if externalProperties[key].startsWith("http")} {#if externalProperties[key].startsWith("http")}
<a href={externalProperties[key]} target="_blank"> <a href={externalProperties[key]} target="_blank">

View file

@ -26,23 +26,21 @@
let externalKeys: string[] = Object.keys(externalProperties).sort() let externalKeys: string[] = Object.keys(externalProperties).sort()
const imageKeyRegex = /image|image:[0-9]+/ const imageKeyRegex = /image|image:[0-9]+/
console.log("Calculating knwon images")
let knownImages = new Set( let knownImages = new Set(
Object.keys(osmProperties) Object.keys(osmProperties)
.filter((k) => k.match(imageKeyRegex)) .filter((k) => k.match(imageKeyRegex))
.map((k) => osmProperties[k]) .map((k) => osmProperties[k])
) )
console.log("Known images are:", knownImages)
let unknownImages = externalKeys let unknownImages = externalKeys
.filter((k) => k.match(imageKeyRegex)) .filter((k) => k.match(imageKeyRegex))
.map((k) => externalProperties[k]) .map((k) => externalProperties[k])
.filter((i) => !knownImages.has(i)) .filter((i) => !knownImages.has(i))
let propertyKeysExternal = externalKeys.filter((k) => k.match(imageKeyRegex) === null) let propertyKeysExternal = externalKeys.filter((k) => k.match(imageKeyRegex) === null)
let missing = propertyKeysExternal.filter((k) => osmProperties[k] === undefined) let missing = propertyKeysExternal.filter((k) => osmProperties[k] === undefined && typeof externalProperties[k] === "string")
let same = propertyKeysExternal.filter((key) => osmProperties[key] === externalProperties[key]) let same = propertyKeysExternal.filter((key) => osmProperties[key] === externalProperties[key])
let different = propertyKeysExternal.filter( let different = propertyKeysExternal.filter(
(key) => osmProperties[key] !== undefined && osmProperties[key] !== externalProperties[key] (key) => osmProperties[key] !== undefined && osmProperties[key] !== externalProperties[key] && typeof externalProperties[key] === "string"
) )
let currentStep: "init" | "applying_all" | "all_applied" = "init" let currentStep: "init" | "applying_all" | "all_applied" = "init"
@ -68,11 +66,7 @@
<th>External</th> <th>External</th>
</tr> </tr>
{#each different as key} {#each different as key}
<tr> <ComparisonAction {key} {state} {tags} {externalProperties} {layer} {feature} {readonly} />
<td>{key}</td>
<td>{osmProperties[key]}</td>
<td>{externalProperties[key]}</td>
</tr>
{/each} {/each}
</table> </table>
{/if} {/if}

View file

@ -3,63 +3,32 @@
* The comparison tool loads json-data from a speficied URL, eventually post-processes it * The comparison tool loads json-data from a speficied URL, eventually post-processes it
* and compares it with the current object * and compares it with the current object
*/ */
import { onMount } from "svelte"
import { Utils } from "../../Utils"
import VeloparkLoader from "../../Logic/Web/VeloparkLoader"
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization" import type { SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import ComparisonTable from "./ComparisonTable.svelte" import ComparisonTable from "./ComparisonTable.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import type { Feature } from "geojson" import type { Feature } from "geojson"
import type { OsmTags } from "../../Models/OsmFeature" import type { OsmTags } from "../../Models/OsmFeature"
export let url: string export let externalData: Store<{ success: {content: Record<string, string> } } | { error: string } | undefined | null /* null if no URL is found, undefined if loading*/>
export let postprocessVelopark: boolean
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let tags: UIEventSource<OsmTags> export let tags: UIEventSource<OsmTags>
export let layer: LayerConfig export let layer: LayerConfig
export let feature: Feature export let feature: Feature
export let readonly = false export let readonly = false
let data: any = undefined
let error: any = undefined
onMount(async () => {
const _url = tags.data[url]
if (!_url) {
error = "No URL found in attribute" + url
}
try {
console.log("Attempting to download", _url)
const downloaded = await Utils.downloadJsonAdvanced(_url)
if (downloaded["error"]) {
console.error(downloaded)
error = downloaded["error"]
return
}
if (postprocessVelopark) {
data = VeloparkLoader.convert(downloaded["content"])
return
}
data = downloaded["content"]
} catch (e) {
console.error(e)
error = "" + e
}
})
</script> </script>
{#if $externalData === null}
{#if error !== undefined} <!-- empty block -->
{:else if $externalData === undefined}
<Loading>{$externalData}</Loading>
{:else if $externalData["error"] !== undefined}
<div class="alert"> <div class="alert">
Something went wrong: {error} Something went wrong: {$externalData["error"]}
</div> </div>
{:else if data === undefined} {:else if $externalData["success"] !== undefined}
<Loading>
Loading {$tags[url]}
</Loading>
{:else if data.properties !== undefined}
<ComparisonTable <ComparisonTable
externalProperties={data.properties} externalProperties={$externalData["success"]}
osmProperties={$tags} osmProperties={$tags}
{state} {state}
{feature} {feature}

View file

@ -7,7 +7,7 @@
import { Mapillary } from "../../Logic/ImageProviders/Mapillary" import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
export let image: ProvidedImage export let image: Partial<ProvidedImage>
let fallbackImage: string = undefined let fallbackImage: string = undefined
if (image.provider === Mapillary.singleton) { if (image.provider === Mapillary.singleton) {
fallbackImage = "./assets/svg/blocked.svg" fallbackImage = "./assets/svg/blocked.svg"

View file

@ -41,6 +41,11 @@ export default class UrlValidator extends Validator {
"AdGroup", "AdGroup",
"TargetId", "TargetId",
"msclkid", "msclkid",
"pk_source",
"pk_medium",
"pk_campaign",
"pk_content",
"pk_kwd"
] ]
for (const dontLike of blacklistedTrackingParams) { for (const dontLike of blacklistedTrackingParams) {
url.searchParams.delete(dontLike.toLowerCase()) url.searchParams.delete(dontLike.toLowerCase())

View file

@ -1,90 +0,0 @@
<script lang="ts">
/**
* Shows attributes that are loaded via linked data and which are suitable for import
*/
import type { SpecialVisualizationState } from "./SpecialVisualization"
import type { Store } from "../Logic/UIEventSource"
import { Stores, UIEventSource } from "../Logic/UIEventSource"
import type { Feature, Geometry } from "geojson"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import LinkedDataLoader from "../Logic/Web/LinkedDataLoader"
import Loading from "./Base/Loading.svelte"
import { GeoOperations } from "../Logic/GeoOperations"
import { OH } from "./OpeningHours/OpeningHours"
export let state: SpecialVisualizationState
export let tagsSource: UIEventSource<Record<string, string>>
export let argument: string[]
export let feature: Feature
export let layer: LayerConfig
export let key: string
let url = tagsSource.mapD(tags => {
if (!tags._country || !tags[key] || tags[key] === "undefined") {
return undefined
}
return ({ url: tags[key], country: tags._country })
})
let dataWithErr = url.bindD(({ url, country }) => {
return Stores.FromPromiseWithErr(LinkedDataLoader.fetchJsonLd(url, country))
})
let error = dataWithErr.mapD(d => d["error"])
let data = dataWithErr.mapD(d => d["success"])
let distanceToFeature: Store<string> = data.mapD(d => d.geo).mapD(geo => {
const dist = Math.round(GeoOperations.distanceBetween(
GeoOperations.centerpointCoordinates(<Geometry>geo), GeoOperations.centerpointCoordinates(feature)))
return dist + "m"
})
let dataCleaned = data.mapD(d => {
const featureTags = tagsSource.data
console.log("Downloaded data is", d)
d = { ...d }
delete d["@context"]
for (const k in d) {
const v = featureTags[k]
if (!v) {
continue
}
if (k === "opening_hours") {
const oh = [].concat(...v.split(";").map(r => OH.ParseRule(r) ?? []))
const merged = OH.ToString(OH.MergeTimes(oh ?? []))
if (merged === d[k]) {
delete d[k]
continue
}
}
if (featureTags[k] === d[k]) {
delete d[k]
}
delete d.geo
}
return d
}, [tagsSource])
</script>
{#if $error}
<div class="alert">
{$error}
</div>
{:else if $url}
<div class="flex flex-col border border-dashed border-gray-500 p-1">
{#if $dataCleaned !== undefined && Object.keys($dataCleaned).length === 0}
No new data from website
{:else if !$data}
<Loading />
{:else}
{$distanceToFeature}
<ul>
{#each Object.keys($dataCleaned) as k}
<li>
<b>{k}</b>: {JSON.stringify($dataCleaned[k])} {$tagsSource[k]} {($dataCleaned[k]) === $tagsSource[k]}
</li>
{/each}
</ul>
{/if}
</div>
{/if}

View file

@ -459,7 +459,7 @@ class LineRenderingLayer {
} else { } else {
const tags = this._fetchStore(id) const tags = this._fetchStore(id)
this._listenerInstalledOn.add(id) this._listenerInstalledOn.add(id)
tags.addCallbackAndRunD((properties) => { tags?.addCallbackAndRunD((properties) => {
// Make sure to use 'getSource' here, the layer names are different! // Make sure to use 'getSource' here, the layer names are different!
try { try {
if (map.getSource(this._layername) === undefined) { if (map.getSource(this._layername) === undefined) {

View file

@ -3,11 +3,7 @@ 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 { import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
RenderingSpecification,
SpecialVisualization,
SpecialVisualizationState,
} from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz" import { HistogramViz } from "./Popup/HistogramViz"
import { MinimapViz } from "./Popup/MinimapViz" import { MinimapViz } from "./Popup/MinimapViz"
import { ShareLinkViz } from "./Popup/ShareLinkViz" import { ShareLinkViz } from "./Popup/ShareLinkViz"
@ -93,7 +89,7 @@ import SpecialVisualisationUtils from "./SpecialVisualisationUtils"
import LoginButton from "./Base/LoginButton.svelte" import LoginButton from "./Base/LoginButton.svelte"
import Toggle from "./Input/Toggle" import Toggle from "./Input/Toggle"
import ImportReviewIdentity from "./Reviews/ImportReviewIdentity.svelte" import ImportReviewIdentity from "./Reviews/ImportReviewIdentity.svelte"
import LinkedDataDisplay from "./LinkedDataDisplay.svelte" import LinkedDataLoader from "../Logic/Web/LinkedDataLoader"
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
@ -120,7 +116,7 @@ class NearbyImageVis implements SpecialVisualization {
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const isOpen = args[0] === "open" const isOpen = args[0] === "open"
const readonly = args[1] === "readonly" const readonly = args[1] === "readonly"
@ -187,7 +183,7 @@ class StealViz implements SpecialVisualization {
selectedElement: otherFeature, selectedElement: otherFeature,
state, state,
layer, layer,
}) }),
) )
} }
if (elements.length === 1) { if (elements.length === 1) {
@ -195,8 +191,8 @@ class StealViz implements SpecialVisualization {
} }
return new Combine(elements).SetClass("flex flex-col") return new Combine(elements).SetClass("flex flex-col")
}, },
[state.indexedFeatures.featuresById] [state.indexedFeatures.featuresById],
) ),
) )
} }
@ -235,7 +231,7 @@ export class QuestionViz implements SpecialVisualization {
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const labels = args[0] const labels = args[0]
?.split(";") ?.split(";")
@ -278,7 +274,7 @@ export default class SpecialVisualizations {
defaultArg = "_empty string_" defaultArg = "_empty string_"
} }
return [arg.name, defaultArg, arg.doc] return [arg.name, defaultArg, arg.doc]
}) }),
) )
: undefined, : undefined,
new Title("Example usage of " + viz.funcName, 4), new Title("Example usage of " + viz.funcName, 4),
@ -288,21 +284,21 @@ export default class SpecialVisualizations {
viz.funcName + viz.funcName +
"(" + "(" +
viz.args.map((arg) => arg.defaultValue).join(",") + viz.args.map((arg) => arg.defaultValue).join(",") +
")}`" ")}`",
).SetClass("literal-code"), ).SetClass("literal-code"),
]) ])
} }
public static constructSpecification( public static constructSpecification(
template: string, template: string,
extraMappings: SpecialVisualization[] = [] extraMappings: SpecialVisualization[] = [],
): RenderingSpecification[] { ): RenderingSpecification[] {
return SpecialVisualisationUtils.constructSpecification(template, extraMappings) return SpecialVisualisationUtils.constructSpecification(template, extraMappings)
} }
public static HelpMessage() { public static HelpMessage() {
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) => const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
SpecialVisualizations.DocumentationFor(viz) SpecialVisualizations.DocumentationFor(viz),
) )
return new Combine([ return new Combine([
@ -336,10 +332,10 @@ export default class SpecialVisualizations {
}, },
}, },
null, null,
" " " ",
) ),
).SetClass("code"), ).SetClass("code"),
'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)', "In other words: use `{ \"before\": ..., \"after\": ..., \"special\": {\"type\": ..., \"argname\": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)",
]).SetClass("flex flex-col"), ]).SetClass("flex flex-col"),
...helpTexts, ...helpTexts,
]).SetClass("flex flex-col") ]).SetClass("flex flex-col")
@ -348,7 +344,7 @@ export default class SpecialVisualizations {
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
public static renderExampleOfSpecial( public static renderExampleOfSpecial(
state: SpecialVisualizationState, state: SpecialVisualizationState,
s: SpecialVisualization s: SpecialVisualization,
): BaseUIElement { ): BaseUIElement {
const examples = const examples =
s.structuredExamples === undefined s.structuredExamples === undefined
@ -359,7 +355,7 @@ export default class SpecialVisualizations {
new UIEventSource<Record<string, string>>(e.feature.properties), new UIEventSource<Record<string, string>>(e.feature.properties),
e.args, e.args,
e.feature, e.feature,
undefined undefined,
) )
}) })
return new Combine([new Title(s.funcName), s.docs, ...examples]) return new Combine([new Title(s.funcName), s.docs, ...examples])
@ -401,7 +397,7 @@ export default class SpecialVisualizations {
assignTo: state.userRelatedState.language, assignTo: state.userRelatedState.language,
availableLanguages: state.layout.language, availableLanguages: state.layout.language,
preferredLanguages: state.osmConnection.userDetails.map( preferredLanguages: state.osmConnection.userDetails.map(
(ud) => ud.languages (ud) => ud.languages,
), ),
}) })
}, },
@ -426,7 +422,7 @@ export default class SpecialVisualizations {
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>> tagSource: UIEventSource<Record<string, string>>,
): BaseUIElement { ): BaseUIElement {
return new VariableUiElement( return new VariableUiElement(
tagSource tagSource
@ -436,7 +432,7 @@ export default class SpecialVisualizations {
return new SplitRoadWizard(<WayId>id, state) return new SplitRoadWizard(<WayId>id, state)
} }
return undefined return undefined
}) }),
) )
}, },
}, },
@ -450,7 +446,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
if (feature.geometry.type !== "Point") { if (feature.geometry.type !== "Point") {
return undefined return undefined
@ -473,7 +469,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
if (!layer.deletion) { if (!layer.deletion) {
return undefined return undefined
@ -501,7 +497,7 @@ export default class SpecialVisualizations {
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature feature: Feature,
): BaseUIElement { ): BaseUIElement {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature) const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(CreateNewNote, { return new SvelteUIElement(CreateNewNote, {
@ -565,7 +561,7 @@ export default class SpecialVisualizations {
.map((tags) => tags[args[0]]) .map((tags) => tags[args[0]])
.map((wikidata) => { .map((wikidata) => {
wikidata = Utils.NoEmpty( wikidata = Utils.NoEmpty(
wikidata?.split(";")?.map((wd) => wd.trim()) ?? [] wikidata?.split(";")?.map((wd) => wd.trim()) ?? [],
)[0] )[0]
const entry = Wikidata.LoadWikidataEntry(wikidata) const entry = Wikidata.LoadWikidataEntry(wikidata)
return new VariableUiElement( return new VariableUiElement(
@ -575,9 +571,9 @@ export default class SpecialVisualizations {
} }
const response = <WikidataResponse>e["success"] const response = <WikidataResponse>e["success"]
return Translation.fromMap(response.labels) return Translation.fromMap(response.labels)
}) }),
) )
}) }),
), ),
}, },
new MapillaryLinkVis(), new MapillaryLinkVis(),
@ -591,7 +587,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
_, _,
__, __,
layer: LayerConfig layer: LayerConfig,
) => new SvelteUIElement(AllTagsPanel, { tags, layer }), ) => new SvelteUIElement(AllTagsPanel, { tags, layer }),
}, },
{ {
@ -613,7 +609,7 @@ export default class SpecialVisualizations {
return new ImageCarousel( return new ImageCarousel(
AllImageProviders.LoadImagesFor(tags, imagePrefixes), AllImageProviders.LoadImagesFor(tags, imagePrefixes),
tags, tags,
state state,
) )
}, },
}, },
@ -669,7 +665,7 @@ export default class SpecialVisualizations {
{ {
nameKey: nameKey, nameKey: nameKey,
fallbackName, fallbackName,
} },
) )
return new SvelteUIElement(StarsBarIcon, { return new SvelteUIElement(StarsBarIcon, {
score: reviews.average, score: reviews.average,
@ -702,7 +698,7 @@ export default class SpecialVisualizations {
{ {
nameKey: nameKey, nameKey: nameKey,
fallbackName, fallbackName,
} },
) )
return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer }) return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer })
}, },
@ -734,7 +730,7 @@ export default class SpecialVisualizations {
{ {
nameKey: nameKey, nameKey: nameKey,
fallbackName, fallbackName,
} },
) )
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer }) return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
}, },
@ -754,7 +750,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const [text] = argument const [text] = argument
return new SvelteUIElement(ImportReviewIdentity, { state, text }) return new SvelteUIElement(ImportReviewIdentity, { state, text })
@ -813,7 +809,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): SvelteUIElement { ): SvelteUIElement {
const keyToUse = args[0] const keyToUse = args[0]
const prefix = args[1] const prefix = args[1]
@ -850,17 +846,17 @@ export default class SpecialVisualizations {
return undefined return undefined
} }
const allUnits: Unit[] = [].concat( const allUnits: Unit[] = [].concat(
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []) ...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []),
) )
const unit = allUnits.filter((unit) => const unit = allUnits.filter((unit) =>
unit.isApplicableToKey(key) unit.isApplicableToKey(key),
)[0] )[0]
if (unit === undefined) { if (unit === undefined) {
return value return value
} }
const getCountry = () => tagSource.data._country const getCountry = () => tagSource.data._country
return unit.asHumanLongValue(value, getCountry) return unit.asHumanLongValue(value, getCountry)
}) }),
) )
}, },
}, },
@ -877,7 +873,7 @@ export default class SpecialVisualizations {
new Combine([ new Combine([
t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"),
t.downloadGeoJsonHelper.SetClass("subtle"), t.downloadGeoJsonHelper.SetClass("subtle"),
]).SetClass("flex flex-col") ]).SetClass("flex flex-col"),
) )
.onClick(() => { .onClick(() => {
console.log("Exporting as Geojson") console.log("Exporting as Geojson")
@ -890,7 +886,7 @@ export default class SpecialVisualizations {
title + "_mapcomplete_export.geojson", title + "_mapcomplete_export.geojson",
{ {
mimetype: "application/vnd.geo+json", mimetype: "application/vnd.geo+json",
} },
) )
}) })
.SetClass("w-full") .SetClass("w-full")
@ -926,7 +922,7 @@ export default class SpecialVisualizations {
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"),
Translations.t.general.removeLocationHistory Translations.t.general.removeLocationHistory,
).onClick(() => { ).onClick(() => {
state.historicalUserLocations.features.setData([]) state.historicalUserLocations.features.setData([])
state.selectedElement.setData(undefined) state.selectedElement.setData(undefined)
@ -964,10 +960,10 @@ export default class SpecialVisualizations {
.filter((c) => c.text !== "") .filter((c) => c.text !== "")
.map( .map(
(c, i) => (c, i) =>
new NoteCommentElement(c, state, i, comments.length) new NoteCommentElement(c, state, i, comments.length),
) ),
).SetClass("flex flex-col") ).SetClass("flex flex-col")
}) }),
), ),
}, },
{ {
@ -1001,7 +997,7 @@ export default class SpecialVisualizations {
tagsSource: UIEventSource<Record<string, string>>, tagsSource: UIEventSource<Record<string, string>>,
_: string[], _: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
) => ) =>
new VariableUiElement( new VariableUiElement(
tagsSource.map((tags) => { tagsSource.map((tags) => {
@ -1019,7 +1015,7 @@ export default class SpecialVisualizations {
feature, feature,
layer, layer,
}).SetClass("px-1") }).SetClass("px-1")
}) }),
), ),
}, },
{ {
@ -1035,8 +1031,8 @@ export default class SpecialVisualizations {
let challenge = Stores.FromPromise( let challenge = Stores.FromPromise(
Utils.downloadJsonCached( Utils.downloadJsonCached(
`${Maproulette.defaultEndpoint}/challenge/${parentId}`, `${Maproulette.defaultEndpoint}/challenge/${parentId}`,
24 * 60 * 60 * 1000 24 * 60 * 60 * 1000,
) ),
) )
return new VariableUiElement( return new VariableUiElement(
@ -1061,7 +1057,7 @@ export default class SpecialVisualizations {
} else { } else {
return [title, new List(listItems)] return [title, new List(listItems)]
} }
}) }),
) )
}, },
docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.", docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.",
@ -1075,15 +1071,15 @@ export default class SpecialVisualizations {
"\n" + "\n" +
"```json\n" + "```json\n" +
"{\n" + "{\n" +
' "id": "mark_duplicate",\n' + " \"id\": \"mark_duplicate\",\n" +
' "render": {\n' + " \"render\": {\n" +
' "special": {\n' + " \"special\": {\n" +
' "type": "maproulette_set_status",\n' + " \"type\": \"maproulette_set_status\",\n" +
' "message": {\n' + " \"message\": {\n" +
' "en": "Mark as not found or false positive"\n' + " \"en\": \"Mark as not found or false positive\"\n" +
" },\n" + " },\n" +
' "status": "2",\n' + " \"status\": \"2\",\n" +
' "image": "close"\n' + " \"image\": \"close\"\n" +
" }\n" + " }\n" +
" }\n" + " }\n" +
"}\n" + "}\n" +
@ -1163,8 +1159,8 @@ export default class SpecialVisualizations {
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox) const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
return new StatisticsPanel(fsBboxed) return new StatisticsPanel(fsBboxed)
}, },
[state.mapProperties.bounds] [state.mapProperties.bounds],
) ),
) )
}, },
}, },
@ -1230,7 +1226,7 @@ export default class SpecialVisualizations {
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
args: string[] args: string[],
): BaseUIElement { ): BaseUIElement {
let [text, href, classnames, download, ariaLabel] = args let [text, href, classnames, download, ariaLabel] = args
if (download === "") { if (download === "") {
@ -1244,14 +1240,14 @@ export default class SpecialVisualizations {
text: Utils.SubstituteKeys(text, tags), text: Utils.SubstituteKeys(text, tags),
href: Utils.SubstituteKeys(href, tags).replaceAll( href: Utils.SubstituteKeys(href, tags).replaceAll(
/ /g, / /g,
"%20" "%20",
) /* Chromium based browsers eat the spaces */, ) /* Chromium based browsers eat the spaces */,
classnames, classnames,
download: Utils.SubstituteKeys(download, tags), download: Utils.SubstituteKeys(download, tags),
ariaLabel: Utils.SubstituteKeys(ariaLabel, tags), ariaLabel: Utils.SubstituteKeys(ariaLabel, tags),
newTab, newTab,
}) }),
) ),
) )
}, },
}, },
@ -1273,7 +1269,7 @@ export default class SpecialVisualizations {
}, },
}, },
null, null,
" " " ",
) + ) +
"\n```", "\n```",
args: [ args: [
@ -1297,7 +1293,7 @@ export default class SpecialVisualizations {
featureTags: UIEventSource<Record<string, string>>, featureTags: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
) { ) {
const [key, tr, classesRaw] = args const [key, tr, classesRaw] = args
let classes = classesRaw ?? "" let classes = classesRaw ?? ""
@ -1322,7 +1318,7 @@ export default class SpecialVisualizations {
elements.push(subsTr) elements.push(subsTr)
} }
return elements return elements
}) }),
) )
}, },
}, },
@ -1342,7 +1338,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new VariableUiElement( return new VariableUiElement(
tagSource.map((tags) => { tagSource.map((tags) => {
@ -1354,7 +1350,7 @@ export default class SpecialVisualizations {
console.error("Cannot create a translation for", v, "due to", e) console.error("Cannot create a translation for", v, "due to", e)
return JSON.stringify(v) return JSON.stringify(v)
} }
}) }),
) )
}, },
}, },
@ -1374,7 +1370,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const key = argument[0] const key = argument[0]
const validator = new FediverseValidator() const validator = new FediverseValidator()
@ -1384,7 +1380,7 @@ export default class SpecialVisualizations {
.map((fediAccount) => { .map((fediAccount) => {
fediAccount = validator.reformat(fediAccount) fediAccount = validator.reformat(fediAccount)
const [_, username, host] = fediAccount.match( const [_, username, host] = fediAccount.match(
FediverseValidator.usernameAtServer FediverseValidator.usernameAtServer,
) )
const normalLink = new SvelteUIElement(Link, { const normalLink = new SvelteUIElement(Link, {
@ -1399,7 +1395,7 @@ export default class SpecialVisualizations {
] ]
console.log( console.log(
"LoggedinContributorMastodon", "LoggedinContributorMastodon",
loggedInContributorMastodon loggedInContributorMastodon,
) )
if (!loggedInContributorMastodon) { if (!loggedInContributorMastodon) {
return normalLink return normalLink
@ -1415,7 +1411,7 @@ export default class SpecialVisualizations {
newTab: true, newTab: true,
}).SetClass("button"), }).SetClass("button"),
]) ])
}) }),
) )
}, },
}, },
@ -1435,7 +1431,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new FixedUiElement("{" + args[0] + "}") return new FixedUiElement("{" + args[0] + "}")
}, },
@ -1456,7 +1452,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const key = argument[0] ?? "value" const key = argument[0] ?? "value"
return new VariableUiElement( return new VariableUiElement(
@ -1476,10 +1472,10 @@ export default class SpecialVisualizations {
"Could not parse this tag: " + "Could not parse this tag: " +
JSON.stringify(value) + JSON.stringify(value) +
" due to " + " due to " +
e e,
).SetClass("alert") ).SetClass("alert")
} }
}) }),
) )
}, },
}, },
@ -1500,7 +1496,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const giggityUrl = argument[0] const giggityUrl = argument[0]
return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl }) return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl })
@ -1516,12 +1512,12 @@ export default class SpecialVisualizations {
_: UIEventSource<Record<string, string>>, _: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const tags = (<ThemeViewState>( const tags = (<ThemeViewState>(
state state
)).geolocation.currentUserLocation.features.map( )).geolocation.currentUserLocation.features.map(
(features) => features[0]?.properties (features) => features[0]?.properties,
) )
return new Combine([ return new Combine([
new SvelteUIElement(OrientationDebugPanel, {}), new SvelteUIElement(OrientationDebugPanel, {}),
@ -1543,7 +1539,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new SvelteUIElement(MarkAsFavourite, { return new SvelteUIElement(MarkAsFavourite, {
tags: tagSource, tags: tagSource,
@ -1563,7 +1559,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new SvelteUIElement(MarkAsFavouriteMini, { return new SvelteUIElement(MarkAsFavouriteMini, {
tags: tagSource, tags: tagSource,
@ -1583,7 +1579,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new SvelteUIElement(DirectionIndicator, { state, feature }) return new SvelteUIElement(DirectionIndicator, { state, feature })
}, },
@ -1598,7 +1594,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new VariableUiElement( return new VariableUiElement(
tagSource tagSource
@ -1620,9 +1616,9 @@ export default class SpecialVisualizations {
`${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` + `${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` +
`#${id}` `#${id}`
return new Img(new Qr(url).toImageElement(75)).SetStyle( return new Img(new Qr(url).toImageElement(75)).SetStyle(
"width: 75px" "width: 75px",
) )
}) }),
) )
}, },
}, },
@ -1642,7 +1638,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const key = args[0] === "" ? "_direction:centerpoint" : args[0] const key = args[0] === "" ? "_direction:centerpoint" : args[0]
return new VariableUiElement( return new VariableUiElement(
@ -1653,11 +1649,11 @@ export default class SpecialVisualizations {
}) })
.mapD((value) => { .mapD((value) => {
const dir = GeoOperations.bearingToHuman( const dir = GeoOperations.bearingToHuman(
GeoOperations.parseBearing(value) GeoOperations.parseBearing(value),
) )
console.log("Human dir", dir) console.log("Human dir", dir)
return Translations.t.general.visualFeedback.directionsAbsolute[dir] return Translations.t.general.visualFeedback.directionsAbsolute[dir]
}) }),
) )
}, },
}, },
@ -1675,11 +1671,6 @@ export default class SpecialVisualizations {
required: true, required: true,
doc: "The domain name(s) where data might be fetched from - this is needed to set the CSP. A domain must include 'https', e.g. 'https://example.com'. For multiple domains, separate them with ';'. If you don't know the possible domains, use '*'. ", doc: "The domain name(s) where data might be fetched from - this is needed to set the CSP. A domain must include 'https', e.g. 'https://example.com'. For multiple domains, separate them with ';'. If you don't know the possible domains, use '*'. ",
}, },
{
name: "postprocessing",
required: false,
doc: "Apply some postprocessing. Currently, only 'velopark' is allowed as value",
},
{ {
name: "readonly", name: "readonly",
required: false, required: false,
@ -1692,19 +1683,19 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const url = args[0] const url = args[0]
const postprocessVelopark = args[2] === "velopark"
const readonly = args[3] === "yes" const readonly = args[3] === "yes"
const externalData = Stores.FromPromiseWithErr(Utils.downloadJson(url))
return new SvelteUIElement(ComparisonTool, { return new SvelteUIElement(ComparisonTool, {
url, url,
postprocessVelopark,
state, state,
tags: tagSource, tags: tagSource,
layer, layer,
feature, feature,
readonly, readonly,
externalData,
}) })
}, },
}, },
@ -1718,12 +1709,12 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new Toggle( return new Toggle(
undefined, undefined,
new SvelteUIElement(LoginButton), new SvelteUIElement(LoginButton),
state.osmConnection.isLoggedIn state.osmConnection.isLoggedIn,
) )
}, },
}, },
@ -1740,19 +1731,36 @@ export default class SpecialVisualizations {
needsUrls: [Constants.linkedDataProxy], needsUrls: [Constants.linkedDataProxy],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagsSource: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const key = argument[0] ?? "website" const key = argument[0] ?? "website"
return new SvelteUIElement(LinkedDataDisplay, { let url = tags.mapD(tags => {
if (!tags._country || !tags[key] || tags[key] === "undefined") {
return null
}
return ({ url: tags[key], country: tags._country })
})
const externalData: Store<{ success: { content: any } } | {
error: string
} | undefined | null> = url.bindD(({
url,
country,
}) => Stores.FromPromiseWithErr(LinkedDataLoader.fetchJsonLdWithProxy(url, { country })))
return new Toggle(
new SvelteUIElement(ComparisonTool, {
feature, feature,
state, state,
tagsSource, tags,
key,
layer, layer,
}) externalData,
}), undefined, url.map(url => !!url),
)
}, },
}, },
] ]
@ -1766,7 +1774,7 @@ export default class SpecialVisualizations {
throw ( throw (
"Invalid special visualisation found: funcName is undefined for " + "Invalid special visualisation found: funcName is undefined for " +
invalid.map((sp) => sp.i).join(", ") + invalid.map((sp) => sp.i).join(", ") +
'. Did you perhaps type \n funcName: "funcname" // type declaration uses COLON\ninstead of:\n funcName = "funcName" // value definition uses EQUAL' ". Did you perhaps type \n funcName: \"funcname\" // type declaration uses COLON\ninstead of:\n funcName = \"funcName\" // value definition uses EQUAL"
) )
} }