Fix(linkeddata): velopark deals with sections, fix image loading

This commit is contained in:
Pieter Vander Vennet 2024-12-17 03:31:28 +01:00
parent 7a06bb9930
commit ef1d2c9f56
8 changed files with 242 additions and 123 deletions

View file

@ -17,7 +17,7 @@
"nl": "Een hulpmiddel om data van velopark.be in OpenStreetMap in te laden" "nl": "Een hulpmiddel om data van velopark.be in OpenStreetMap in te laden"
}, },
"descriptionTail": { "descriptionTail": {
"*": "<h3>Maintainer tools</h3><ul class='link-underline'><li><a target='_blank' href='https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Themes/velopark.md'>See documentation and links to Overpass</a></li><li><a href='https://maproulette.org/api/v2/challenge/view/43282' download='Velopark_sync_2024-01-15.geojson'>Download the first sync results</a></li><li><a href='http://overpass-turbo.eu/?Q=%5Bout%3Ajson%5D%5Btimeout%3A90%5D%3B%28%20%20%20%20nwr%5B%22amenity%22%3D%22bicycle_parking%22%5D%5B%22ref%3Avelopark%22%5D%28%7B%7Bbbox%7D%7D%29%3B%0A%29%3Bout%20body%3B%3E%3Bout%20skel%20qt%3B' target='_blank'>See all bicycle parkings with a velopark ref</a>To export: visit this link, click 'run' and then 'export'; 'export as geojson'</ul>" "*": "<h3>Maintainer tools</h3><ul class='link-underline'><li><a target='_blank' href='https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Themes/velopark.md'>See documentation and links to Overpass</a></li><li><a href='https://maproulette.org/api/v2/challenge/view/43282' download='Velopark_sync_2024-01-15.geojson'>Download the first batch results</a></li><li><a href='https://maproulette.org/api/v2/challenge/view/50552'>Download the second batch results</a></li><li><a href='http://overpass-turbo.eu/?Q=%5Bout%3Ajson%5D%5Btimeout%3A90%5D%3B%28%20%20%20%20nwr%5B%22amenity%22%3D%22bicycle_parking%22%5D%5B%22ref%3Avelopark%22%5D%28%7B%7Bbbox%7D%7D%29%3B%0A%29%3Bout%20body%3B%3E%3Bout%20skel%20qt%3B' target='_blank'>See all bicycle parkings with a velopark ref</a>To export: visit this link, click 'run' and then 'export'; 'export as geojson'</ul>"
}, },
"icon": "./assets/themes/velopark/velopark.svg", "icon": "./assets/themes/velopark/velopark.svg",
"startZoom": 18, "startZoom": 18,
@ -31,7 +31,7 @@
"description": "Maproulette challenge containing velopark data", "description": "Maproulette challenge containing velopark data",
"source": { "source": {
"osmTags": "mr_taskId~*", "osmTags": "mr_taskId~*",
"geoJson": "https://maproulette.org/api/v2/challenge/view/43282", "geoJson": "https://maproulette.org/api/v2/challenge/view/50552",
"idKey": "mr_taskId" "idKey": "mr_taskId"
}, },
"title": { "title": {
@ -161,6 +161,7 @@
"en": "Create a new bicycle parking in OSM. This parking will have the link, you'll be able to copy the attributes in the next step", "en": "Create a new bicycle parking in OSM. This parking will have the link, you'll be able to copy the attributes in the next step",
"nl": "Maak een nieuwe parking aan in OSM. Deze parking zal gelinkt zijn met Velopark en je kan in de volgende stap de attributen overzetten" "nl": "Maak een nieuwe parking aan in OSM. Deze parking zal gelinkt zijn met Velopark en je kan in de volgende stap de attributen overzetten"
}, },
"to_point": "yes",
"maproulette_id": "mr_taskId" "maproulette_id": "mr_taskId"
} }
} }
@ -238,7 +239,12 @@
} }
} }
], ],
"lineRendering": [], "lineRendering": [
{
"color": "#bb9922",
"lineWidth": 2
}
],
"filter": [ "filter": [
{ {
"id": "created-only", "id": "created-only",

View file

@ -2,17 +2,18 @@ import Script from "../Script"
import fs from "fs" import fs from "fs"
import LinkedDataLoader from "../../src/Logic/Web/LinkedDataLoader" import LinkedDataLoader from "../../src/Logic/Web/LinkedDataLoader"
import { Utils } from "../../src/Utils" import { Utils } from "../../src/Utils"
import { Feature } from "geojson" import { Feature, FeatureCollection, Point } from "geojson"
import { BBox } from "../../src/Logic/BBox" import { BBox } from "../../src/Logic/BBox"
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 { ImmutableStore } from "../../src/Logic/UIEventSource" import { ImmutableStore } from "../../src/Logic/UIEventSource"
import Constants from "../../src/Models/Constants" import Constants from "../../src/Models/Constants"
import { MaprouletteStatus } from "../../src/Logic/Maproulette"
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",
) )
} }
@ -28,8 +29,8 @@ class VeloParkToGeojson extends Script {
} }
: features, : features,
null, null,
" " " ",
) ),
) )
console.log("Written", file, "(" + features.length, " features)") console.log("Written", file, "(" + features.length, " features)")
} }
@ -44,12 +45,15 @@ class VeloParkToGeojson extends Script {
const linkedData = await LinkedDataLoader.fetchVeloparkEntry(url) const linkedData = await LinkedDataLoader.fetchVeloparkEntry(url)
const allVelopark: Feature[] = [] const allVelopark: Feature[] = []
if (linkedData.length > 1) {
console.log("Detected multiple sections in:", url)
}
for (const sectionId in linkedData) { for (const sectionId in linkedData) {
const sectionInfo = linkedData[sectionId] const sectionInfo = linkedData[sectionId]
if (Object.keys(sectionInfo).length === 0) { if (Object.keys(sectionInfo).length === 0) {
console.warn("No result for", url) console.warn("No result for", url)
} }
if (!sectionInfo.geometry?.coordinates) { if (!sectionInfo.geometry?.["coordinates"]) {
throw "Invalid properties!" throw "Invalid properties!"
} }
allVelopark.push(sectionInfo) allVelopark.push(sectionInfo)
@ -62,8 +66,8 @@ class VeloParkToGeojson extends Script {
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 allVeloparkRaw: { url: string }[] = <{ url: string }[]>await Utils.downloadJson(url) const allVeloparkRaw= (await Utils.downloadJson<{ url: string }[]>(url))
// Example multi-entry: https://data.velopark.be/data/Stad-Izegem_IZE_015
let failed = 0 let failed = 0
console.log("Got", allVeloparkRaw.length, "items") console.log("Got", allVeloparkRaw.length, "items")
const allVelopark: Feature[] = [] const allVelopark: Feature[] = []
@ -82,14 +86,15 @@ class VeloParkToGeojson extends Script {
console.error("Loading ", f.url, " failed due to", e) console.error("Loading ", f.url, " failed due to", e)
failed++ failed++
} }
}) }),
) )
console.log("Batch complete:", i)
} }
console.log( console.log(
"Fetching data done, got ", "Fetching data done, got ",
allVelopark.length + "/" + allVeloparkRaw.length, allVelopark.length + "/" + allVeloparkRaw.length,
"failed:", "failed:",
failed failed,
) )
VeloParkToGeojson.exportGeojsonTo("velopark_all", allVelopark) VeloParkToGeojson.exportGeojsonTo("velopark_all", allVelopark)
@ -135,7 +140,37 @@ class VeloParkToGeojson extends Script {
} }
} }
private static async fetchMapRouletteClosedItems() {
const challenges = ["https://maproulette.org/api/v2/challenge/view/43282"]
const solvedRefs: Set<string> = new Set<string>();
for (const url of challenges) {
const data = await Utils.downloadJson<FeatureCollection<Point, {
"mr_taskId": string,
"ref:velopark": string,
mr_taskStatus: MaprouletteStatus,
mr_responses: string | undefined
}>>(url)
for (const challenge of data.features) {
const status = challenge.properties.mr_taskStatus
const isClosed = status === "Fixed" || status === "False_positive" || status === "Already fixed" || status === "Too_Hard" || status === "Deleted"
if(isClosed){
const ref = challenge.properties["ref:velopark"]
solvedRefs .add(ref)
}
}
}
console.log("Detected", solvedRefs,"as closed on mapRoulette")
return solvedRefs
}
/**
* Creates an extra version where all bicycle parkings which are already linked are removed.
* Fetches the latest OSM-data from overpass
* @param allVelopark
* @private
*/
private static async createDiff(allVelopark: Feature[]) { private static async createDiff(allVelopark: Feature[]) {
console.log("Creating diff...")
const bboxBelgium = new BBox([ const bboxBelgium = new BBox([
[2.51357303225, 49.5294835476], [2.51357303225, 49.5294835476],
[6.15665815596, 51.4750237087], [6.15665815596, 51.4750237087],
@ -146,42 +181,71 @@ 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))[0] const alreadyLinkedFeatures = (await alreadyLinkedQuery.queryGeoJson(bboxBelgium))[0]
const seenIds = new Set<string>( const seenIds = new Set<string>(
alreadyLinkedFeatures.features.map((f) => f.properties?.["ref:velopark"]) alreadyLinkedFeatures.features.map((f) => f.properties?.["ref:velopark"]),
) )
this.exportGeojsonTo("osm_with_velopark_link", <Feature[]>alreadyLinkedFeatures.features) this.exportGeojsonTo("osm_with_velopark_link", <Feature[]>alreadyLinkedFeatures.features)
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 features: Feature[] = allVelopark.filter( const features: Feature[] = allVelopark.filter(
(f) => !seenIds.has(f.properties["ref:velopark"]) (f) => !seenIds.has(f.properties["ref:velopark"]),
) )
VeloParkToGeojson.exportGeojsonTo("velopark_nonsynced", features) VeloParkToGeojson.exportGeojsonTo("velopark_nonsynced", features)
const synced =await this.fetchMapRouletteClosedItems()
const featuresMoreFiltered = features.filter(
(f) => !synced.has(f.properties["ref:velopark"])
)
VeloParkToGeojson.exportGeojsonTo("velopark_nonsynced_nonclosed", featuresMoreFiltered)
const featuresMoreFilteredFailed = features.filter(
(f) => synced.has(f.properties["ref:velopark"])
)
VeloParkToGeojson.exportGeojsonTo("velopark_nonsynced_human_import_failed", featuresMoreFilteredFailed)
const allProperties = new Set<string>() const allProperties = new Set<string>()
for (const feature of features) { for (const feature of featuresMoreFiltered) {
Object.keys(feature).forEach((k) => allProperties.add(k)) Object.keys(feature.properties).forEach((k) => allProperties.add(k))
} }
allProperties.delete("ref:velopark") allProperties.delete("ref:velopark")
for (const feature of features) { for (const feature of featuresMoreFiltered) {
allProperties.forEach((k) => { allProperties.forEach((k) => {
delete feature[k] if(k === "ref:velopark"){
return
}
delete feature.properties[k]
}) })
} }
this.exportGeojsonTo("velopark_nonsynced_id_only", features) this.exportGeojsonTo("velopark_nonsynced_nonclosed_id_only", featuresMoreFiltered)
}
public static async findMultiSection(): Promise<string[]> {
const url = "https://www.velopark.be/api/parkings/1000"
const raw = await Utils.downloadJson<{"@graph": {}[], url: string}[]>(url)
const multiEntries: string[] = []
for (const entry of raw) {
if(entry["@graph"].length > 1){
multiEntries.push(entry.url)
}
}
return multiEntries
} }
async main(): Promise<void> { async main(): Promise<void> {
// const multiEntries = new Set(await VeloParkToGeojson.findMultiSection())
const allVelopark = const allVelopark =
VeloParkToGeojson.loadFromFile() ?? (await VeloParkToGeojson.downloadData()) VeloParkToGeojson.loadFromFile() ?? (await VeloParkToGeojson.downloadData())
console.log("Got", allVelopark.length, " items") console.log("Got", allVelopark.length, " items")
VeloParkToGeojson.exportExtraAmenities(allVelopark) VeloParkToGeojson.exportExtraAmenities(allVelopark)
await VeloParkToGeojson.createDiff(allVelopark) await VeloParkToGeojson.createDiff(allVelopark)
console.log( console.log(
"Use vite-node scripts/velopark/compare.ts to compare the results and generate a diff file" "Use vite-node scripts/velopark/compare.ts to compare the results and generate a diff file",
) )
} }
} }

View file

@ -6,6 +6,17 @@ export interface MaprouletteTask {
description: string description: string
instruction: string instruction: string
} }
export const maprouletteStatus = ["Open",
"Fixed",
"False_positive",
"Skipped",
"Deleted",
"Already fixed",
"Too_Hard",
"Disabled",
] as const
export type MaprouletteStatus = typeof maprouletteStatus[number]
export default class Maproulette { export default class Maproulette {
public static readonly defaultEndpoint = "https://maproulette.org/api/v2" public static readonly defaultEndpoint = "https://maproulette.org/api/v2"
@ -19,16 +30,7 @@ export default class Maproulette {
public static readonly STATUS_TOO_HARD = 6 public static readonly STATUS_TOO_HARD = 6
public static readonly STATUS_DISABLED = 9 public static readonly STATUS_DISABLED = 9
public static readonly STATUS_MEANING = {
0: "Open",
1: "Fixed",
2: "False_positive",
3: "Skipped",
4: "Deleted",
5: "Already fixed",
6: "Too_Hard",
9: "Disabled"
}
public static singleton = new Maproulette() public static singleton = new Maproulette()
/* /*
* The API endpoint to use * The API endpoint to use
@ -62,13 +64,12 @@ export default class Maproulette {
if (code === "Created") { if (code === "Created") {
return Maproulette.STATUS_OPEN return Maproulette.STATUS_OPEN
} }
for (let i = 0; i < 9; i++) { const i = maprouletteStatus.findIndex(<any> code)
if (Maproulette.STATUS_MEANING["" + i] === code) { if(i < 0){
return i
}
}
return undefined return undefined
} }
return i
}
/** /**
* Close a task; might throw an error * Close a task; might throw an error
@ -87,7 +88,7 @@ export default class Maproulette {
tags?: string tags?: string
requestReview?: boolean requestReview?: boolean
completionResponses?: Record<string, string> completionResponses?: Record<string, string>
} },
): Promise<void> { ): Promise<void> {
console.log("Maproulette: setting", `${this.endpoint}/task/${taskId}/${status}`, options) console.log("Maproulette: setting", `${this.endpoint}/task/${taskId}/${status}`, options)
options ??= {} options ??= {}
@ -97,9 +98,9 @@ export default class Maproulette {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
apiKey: this.apiKey apiKey: this.apiKey,
}, },
body: JSON.stringify(options) body: JSON.stringify(options),
}) })
if (response.status !== 204) { if (response.status !== 204) {
console.log(`Failed to close task: ${response.status}`) console.log(`Failed to close task: ${response.status}`)

View file

@ -3,40 +3,12 @@ import { Utils } from "../../Utils"
export class ThemeMetaTagging { export class ThemeMetaTagging {
public static readonly themeName = "usersettings" public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) { public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
feat.properties._description Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
?.at(1) Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
) Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
Utils.AddLazyProperty( feat.properties['__current_backgroun'] = 'initial_value'
feat.properties,
"_d",
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
} }
} }

View file

@ -68,7 +68,7 @@ export default class LinkedDataLoader {
coors coors
.trim() .trim()
.split(" ") .split(" ")
.map((n) => Number(n)) .map((n) => Number(n)),
), ),
], ],
} }
@ -156,7 +156,7 @@ export default class LinkedDataLoader {
} }
const compacted = await jsonld.compact( const compacted = await jsonld.compact(
openingHoursSpecification, openingHoursSpecification,
<any>LinkedDataLoader.COMPACTING_CONTEXT_OH <any>LinkedDataLoader.COMPACTING_CONTEXT_OH,
) )
const spec: object = compacted["@graph"] const spec: object = compacted["@graph"]
if (!spec) { if (!spec) {
@ -190,12 +190,12 @@ export default class LinkedDataLoader {
const compacted = await jsonld.compact(data, <any>LinkedDataLoader.COMPACTING_CONTEXT) 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[] = <any>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("; "),
) )
delete compacted["openingHours"] delete compacted["openingHours"]
} }
@ -236,7 +236,7 @@ export default class LinkedDataLoader {
static async fetchJsonLd( static async fetchJsonLd(
url: string, url: string,
options?: JsonLdLoaderOptions, options?: JsonLdLoaderOptions,
mode?: "fetch-lod" | "fetch-raw" | "proxy" mode?: "fetch-lod" | "fetch-raw" | "proxy",
): Promise<object> { ): Promise<object> {
mode ??= "fetch-lod" mode ??= "fetch-lod"
if (mode === "proxy") { if (mode === "proxy") {
@ -251,7 +251,7 @@ export default class LinkedDataLoader {
const div = document.createElement("div") const div = document.createElement("div")
div.innerHTML = htmlContent div.innerHTML = htmlContent
const script = Array.from(div.getElementsByTagName("script")).find( const script = Array.from(div.getElementsByTagName("script")).find(
(script) => script.type === "application/ld+json" (script) => script.type === "application/ld+json",
) )
const snippet = JSON.parse(script.textContent) const snippet = JSON.parse(script.textContent)
@ -266,7 +266,7 @@ export default class LinkedDataLoader {
*/ */
static removeDuplicateData( static removeDuplicateData(
externalData: Record<string, string>, externalData: Record<string, string>,
currentData: Record<string, string> currentData: Record<string, string>,
): Record<string, string> { ): Record<string, string> {
const d = { ...externalData } const d = { ...externalData }
delete d["@context"] delete d["@context"]
@ -332,7 +332,7 @@ export default class LinkedDataLoader {
} }
private static patchVeloparkProperties( private static patchVeloparkProperties(
input: Record<string, Set<string>> input: Record<string, Set<string>>,
): Record<string, string[]> { ): Record<string, string[]> {
const output: Record<string, string[]> = {} const output: Record<string, string[]> = {}
for (const k in input) { for (const k in input) {
@ -472,7 +472,7 @@ export default class LinkedDataLoader {
audience, audience,
"for", "for",
input["ref:velopark"], input["ref:velopark"],
" assuming yes" " assuming yes",
) )
return "yes" return "yes"
}) })
@ -516,8 +516,11 @@ export default class LinkedDataLoader {
private static async fetchVeloparkProperty<T extends string, G extends T>( private static async fetchVeloparkProperty<T extends string, G extends T>(
url: string, url: string,
property: string, property: string,
variable?: string variable?: string,
): Promise<SparqlResult<T, G>> { ): Promise<SparqlResult<T, G>> {
if(property === "schema:photos"){
console.log(">> Getting photos")
}
const results = await new TypedSparql().typedSparql<T, G>( const results = await new TypedSparql().typedSparql<T, G>(
{ {
schema: "http://schema.org/", schema: "http://schema.org/",
@ -529,17 +532,25 @@ export default class LinkedDataLoader {
[url], [url],
undefined, undefined,
" ?parking a <http://schema.mobivoc.org/BicycleParkingStation>", " ?parking a <http://schema.mobivoc.org/BicycleParkingStation>",
"?parking " + property + " " + (variable ?? "") "?parking " + property + " " + (variable ?? ""),
) )
return results return results
} }
/**
*
* @param url
* @param property
* @param subExpr
* @private
*/
private static async fetchVeloparkGraphProperty<T extends string>( private static async fetchVeloparkGraphProperty<T extends string>(
url: string, url: string,
property: string, property: string,
subExpr?: string subExpr?: string,
): Promise<SparqlResult<T, "g">> { ): Promise<SparqlResult<T, "g">> {
return await new TypedSparql().typedSparql<T, "g">( const result = await new TypedSparql().typedSparql<T, "g">(
{ {
schema: "http://schema.org/", schema: "http://schema.org/",
mv: "http://schema.mobivoc.org/", mv: "http://schema.mobivoc.org/",
@ -551,8 +562,10 @@ export default class LinkedDataLoader {
"g", "g",
" ?parking a <http://schema.mobivoc.org/BicycleParkingStation>", " ?parking a <http://schema.mobivoc.org/BicycleParkingStation>",
S.graph("g", "?section " + property + " " + (subExpr ?? ""), "?section a ?type") S.graph("g", "?section " + property + " " + (subExpr ?? ""), "?section a ?type", "BIND(STR(?section) AS ?id)"),
) )
return result
} }
/** /**
@ -569,26 +582,67 @@ export default class LinkedDataLoader {
continue continue
} }
for (const sectionKey in subResult) { for (const sectionKey in subResult) {
if (!r[sectionKey]) { if (sectionKey === "default") {
r[sectionKey] = {} r["default"] ??= {}
} const section = subResult["default"]
const section = subResult[sectionKey]
for (const key in section) { for (const key in section) {
r[sectionKey][key] ??= section[key] r["default"][key] ??= section[key]
}
} else {
const section = subResult[sectionKey]
const actualId = Array.from(section["id"] ?? [])[0] ?? sectionKey
r[actualId] ??= {}
for (const key in section) {
r[actualId][key] ??= section[key]
}
} }
} }
} }
if (r["default"] !== undefined && Object.keys(r).length > 1) { /**
* Copy all values from the section with name "key" into the other sections,
* remove section "key" afterwards
* @param key
*/
function spreadSection(key: string){
for (const section in r) { for (const section in r) {
if (section === "default") { if (section === key) {
continue continue
} }
for (const k in r.default) { for (const k in r[key]) {
r[section][k] ??= r.default[k] r[section][k] ??= r[key][k]
} }
} }
delete r.default delete r[key]
}
// The "default" part of the result contains all general info
// The other 'sections' need to get those copied! Then, we delete the "default"-section
if (r["default"] !== undefined && Object.keys(r).length > 1) {
spreadSection("default")
}
if (Object.keys(r).length > 1) {
// This result has multiple sections
// We should check that the naked URL got distributed and scrapped
const keys = Object.keys(r)
if (Object.keys(r).length > 2) {
console.log("Multiple sections detected: ", JSON.stringify(keys))
}
const shortestKeyLength: number = Math.min(...keys.map(k => k.length))
const key = keys.find(k => k.length === shortestKeyLength)
if (keys.some(k => !k.startsWith(key))) {
throw "Invalid multi-object: the shortest key is not the start of all the others: " + JSON.stringify(keys)
}
spreadSection(key)
}
if (Object.keys(r).length == 1) {
const key = Object.keys(r)[0]
if(key.indexOf("#")>0){
const newKey = key.split("#")[0]
r[newKey] = r[key]
delete r[key]
}
} }
return r return r
} }
@ -597,7 +651,7 @@ export default class LinkedDataLoader {
directUrl: string, directUrl: string,
propertiesWithoutGraph: PropertiesSpec<T>, propertiesWithoutGraph: PropertiesSpec<T>,
propertiesInGraph: PropertiesSpec<T>, propertiesInGraph: PropertiesSpec<T>,
extra?: string[] extra?: string[],
): Promise<SparqlResult<T, string>> { ): Promise<SparqlResult<T, string>> {
const allPartialResults: SparqlResult<T, string>[] = [] const allPartialResults: SparqlResult<T, string>[] = []
for (const propertyName in propertiesWithoutGraph) { for (const propertyName in propertiesWithoutGraph) {
@ -607,7 +661,7 @@ export default class LinkedDataLoader {
const result = await this.fetchVeloparkProperty( const result = await this.fetchVeloparkProperty(
directUrl, directUrl,
propertyName, propertyName,
"?" + variableName "?" + variableName,
) )
allPartialResults.push(result) allPartialResults.push(result)
} else { } else {
@ -616,7 +670,7 @@ export default class LinkedDataLoader {
const result = await this.fetchVeloparkProperty( const result = await this.fetchVeloparkProperty(
directUrl, directUrl,
propertyName, propertyName,
`[${subProperty} ?${variableName}] ` `[${subProperty} ?${variableName}] `,
) )
allPartialResults.push(result) allPartialResults.push(result)
} }
@ -634,7 +688,7 @@ export default class LinkedDataLoader {
const result = await this.fetchVeloparkGraphProperty( const result = await this.fetchVeloparkGraphProperty(
directUrl, directUrl,
propertyName, propertyName,
variableName variableName,
) )
allPartialResults.push(result) allPartialResults.push(result)
} }
@ -646,7 +700,7 @@ export default class LinkedDataLoader {
const result = await this.fetchVeloparkGraphProperty( const result = await this.fetchVeloparkGraphProperty(
directUrl, directUrl,
propertyName, propertyName,
variableName variableName,
) )
allPartialResults.push(result) allPartialResults.push(result)
} else { } else {
@ -655,7 +709,7 @@ export default class LinkedDataLoader {
const result = await this.fetchVeloparkGraphProperty( const result = await this.fetchVeloparkGraphProperty(
directUrl, directUrl,
propertyName, propertyName,
`[${subProperty} ?${variableName}] ` `[${subProperty} ?${variableName}] `,
) )
allPartialResults.push(result) allPartialResults.push(result)
} }
@ -675,16 +729,18 @@ export default class LinkedDataLoader {
/** /**
* Fetches all data relevant to velopark. * Fetches all data relevant to velopark.
* The id will be saved as `ref:velopark` * The id will be saved as `ref:velopark`
* If the entry has multiple sections, this will return multiple items
* @param url * @param url
*/ */
public static async fetchVeloparkEntry( public static async fetchVeloparkEntry(
url: string, url: string,
includeExtras: boolean = false includeExtras: boolean = false,
): Promise<Feature[]> { ): Promise<Feature[]> {
const cacheKey = includeExtras + url const cacheKey = includeExtras + url
if (this.veloparkCache[cacheKey]) { if (this.veloparkCache[cacheKey]) {
return this.veloparkCache[cacheKey] return this.veloparkCache[cacheKey]
} }
// Note: the proxy doesn't make any changes in this case
const withProxyUrl = Constants.linkedDataProxy.replace("{url}", encodeURIComponent(url)) const withProxyUrl = Constants.linkedDataProxy.replace("{url}", encodeURIComponent(url))
const optionalPaths: Record<string, string | Record<string, string>> = { const optionalPaths: Record<string, string | Record<string, string>> = {
"schema:interactionService": { "schema:interactionService": {
@ -697,6 +753,7 @@ export default class LinkedDataLoader {
"schema:email": "email", "schema:email": "email",
"schema:telephone": "phone", "schema:telephone": "phone",
}, },
// "schema:photos": "images",
"schema:dateModified": "_last_edit_timestamp", "schema:dateModified": "_last_edit_timestamp",
} }
if (includeExtras) { if (includeExtras) {
@ -738,8 +795,16 @@ export default class LinkedDataLoader {
withProxyUrl, withProxyUrl,
optionalPaths, optionalPaths,
graphOptionalPaths, graphOptionalPaths,
extra extra,
) )
for (const unpatchedKey in unpatched) {
// Dirty hack
const rawData = await Utils.downloadJsonCached<object>(url, 1000*60*60)
const images = rawData["photos"].map(ph => <string> ph.image)
unpatched[unpatchedKey].images = new Set<string>(images)
}
console.log("Got unpatched:", unpatched)
const patched: Feature[] = [] const patched: Feature[] = []
for (const section in unpatched) { for (const section in unpatched) {
const p = LinkedDataLoader.patchVeloparkProperties(unpatched[section]) const p = LinkedDataLoader.patchVeloparkProperties(unpatched[section])

View file

@ -67,13 +67,11 @@ export default class TypedSparql {
bindings.forEach((item) => { bindings.forEach((item) => {
const result = <Record<VARS | G, Set<string>>>{} const result = <Record<VARS | G, Set<string>>>{}
item.forEach((value, key) => { item.forEach((value, key) => {
if (!result[key.value]) { result[key.value] ??= new Set()
result[key.value] = new Set()
}
result[key.value].add(value.value) result[key.value].add(value.value)
}) })
if (graphVariable && result[graphVariable]?.size > 0) { if (graphVariable && result[graphVariable]?.size > 0) {
const id = Array.from(result[graphVariable])?.[0] ?? "default" const id: string = (<string> Array.from(result["id"] ?? [])?.[0] ?? Array.from(result[graphVariable] ?? [])?.[0]) ?? "default"
resultAllGraphs[id] = result resultAllGraphs[id] = result
} else { } else {
resultAllGraphs["default"] = result resultAllGraphs["default"] = result

View file

@ -5,7 +5,7 @@
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Icon from "../Map/Icon.svelte" import Icon from "../Map/Icon.svelte"
import Maproulette from "../../Logic/Maproulette" import Maproulette, { maprouletteStatus } from "../../Logic/Maproulette"
import LoginToggle from "../Base/LoginToggle.svelte" import LoginToggle from "../Base/LoginToggle.svelte"
/** /**
@ -38,10 +38,11 @@
async function apply() { async function apply() {
const maproulette_id = tags.data[maproulette_id_key] ?? tags.data.mr_taskId ?? tags.data.id const maproulette_id = tags.data[maproulette_id_key] ?? tags.data.mr_taskId ?? tags.data.id
try { try {
await Maproulette.singleton.closeTask(Number(maproulette_id), Number(statusToSet), state, { const statusIndex = Maproulette.codeToIndex(statusToSet) ?? Number(statusToSet)
await Maproulette.singleton.closeTask(Number(maproulette_id), statusIndex, state, {
comment: feedback, comment: feedback,
}) })
tags.data["mr_taskStatus"] = Maproulette.STATUS_MEANING[Number(statusToSet)] tags.data["mr_taskStatus"] = maprouletteStatus[statusIndex]
tags.data.status = statusToSet tags.data.status = statusToSet
tags.ping() tags.ping()
} catch (e) { } catch (e) {

View file

@ -9,6 +9,7 @@ import { PointImportFlowArguments, PointImportFlowState } from "./PointImportFlo
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import { ImportFlowUtils } from "./ImportFlow" import { ImportFlowUtils } from "./ImportFlow"
import Translations from "../../i18n/Translations" import Translations from "../../i18n/Translations"
import { GeoOperations } from "../../../Logic/GeoOperations"
/** /**
* The wrapper to make the special visualisation for the PointImportFlow * The wrapper to make the special visualisation for the PointImportFlow
@ -44,6 +45,10 @@ export class PointImportButtonViz implements SpecialVisualization {
name: "maproulette_id", name: "maproulette_id",
doc: "The property name of the maproulette_id - this is probably `mr_taskId`. If given, the maproulette challenge will be marked as fixed. Only use this if part of a maproulette-layer.", doc: "The property name of the maproulette_id - this is probably `mr_taskId`. If given, the maproulette challenge will be marked as fixed. Only use this if part of a maproulette-layer.",
}, },
{
name: "to_point",
doc: "If set, a feature will be converted to a centerpoint",
},
] ]
} }
@ -51,11 +56,18 @@ export class PointImportButtonViz implements SpecialVisualization {
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 to_point_index = this.args.findIndex(arg => arg.name === "to_point")
const summarizePointArg = argument[to_point_index].toLowerCase()
if (feature.geometry.type !== "Point") { if (feature.geometry.type !== "Point") {
if (summarizePointArg !== "no" && summarizePointArg !== "false") {
feature = GeoOperations.centerpoint(feature)
} else {
return Translations.t.general.add.import.wrongType.SetClass("alert") return Translations.t.general.add.import.wrongType.SetClass("alert")
} }
}
const baseArgs: PointImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument) const baseArgs: PointImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, baseArgs) const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, baseArgs)
const importFlow = new PointImportFlowState( const importFlow = new PointImportFlowState(
@ -63,7 +75,7 @@ export class PointImportButtonViz implements SpecialVisualization {
<Feature<Point>>feature, <Feature<Point>>feature,
baseArgs, baseArgs,
tagsToApply, tagsToApply,
tagSource tagSource,
) )
return new SvelteUIElement(PointImportFlow, { return new SvelteUIElement(PointImportFlow, {