forked from MapComplete/MapComplete
Themes(benches): add script that reads the openbenches-dataset from their git repo, add (experimental) maproulette import layer to benches
This commit is contained in:
parent
872a97d128
commit
3ebef308d6
10 changed files with 354 additions and 31 deletions
|
@ -21,12 +21,12 @@ Hint: MapRoulette has a button 'rebuild task', where you can first 'remove all i
|
||||||
**Most of the heavy lifting is done in [layer `maproulette-challenge`](./Docs/Layers/maproulette_challenge.md). Extend this layer with your needs.**
|
**Most of the heavy lifting is done in [layer `maproulette-challenge`](./Docs/Layers/maproulette_challenge.md). Extend this layer with your needs.**
|
||||||
The API is shortly discussed here for future reference only.
|
The API is shortly discussed here for future reference only.
|
||||||
|
|
||||||
There is an API-endpoint at `https://maproulette.org/api/v2/tasks/box/{x_min}/{y_min}/{x_max}/{y_max}` which can be used
|
Hint: use [the maproulette theme in debug mode](https://mapcomplete.org/maproulette?debug=true) to inspect all properties.
|
||||||
to query _all_ tasks in a bbox and returns this as geojson. Hint:
|
|
||||||
use [the maproulette theme in debug mode](https://mapcomplete.org/maproulette?debug=true) to inspect all properties.
|
|
||||||
|
|
||||||
To view the overview a single challenge, visit `https://maproulette.org/browse/challenges/<challenge-id>` with your
|
To view the overview a single challenge, visit `https://maproulette.org/browse/challenges/<challenge-id>` with your
|
||||||
browser.
|
browser.
|
||||||
|
To get the challenge-id, visit the overview of a challenge in your browser. The URL will end with `/project/<project-id>/challenge/<challenge-id>?`. You can ignore the project-id and use the `challenge-id`. Challenge ids are unique and straight from the database, even over users and projects.
|
||||||
|
|
||||||
The API endpoint for a single challenge is `https://maproulette.org/api/v2/challenge/view/<challenge-id>` which returns a
|
The API endpoint for a single challenge is `https://maproulette.org/api/v2/challenge/view/<challenge-id>` which returns a
|
||||||
geojson. This endpoint supports a bbox to only return a part of the challenge: `https://maproulette.org/api/v2/challenge/view/<challenge-id>?bbox=west,south,east,north`
|
geojson. This endpoint supports a bbox to only return a part of the challenge: `https://maproulette.org/api/v2/challenge/view/<challenge-id>?bbox=west,south,east,north`
|
||||||
|
|
||||||
|
|
|
@ -236,7 +236,8 @@
|
||||||
"nl": "Toon taken die zijn gecreëerd",
|
"nl": "Toon taken die zijn gecreëerd",
|
||||||
"pl": "Pokaż zadania, które zostały stworzone"
|
"pl": "Pokaż zadania, które zostały stworzone"
|
||||||
},
|
},
|
||||||
"osmTags": "mr_taskStatus=Created"
|
"osmTags": "mr_taskStatus=Created",
|
||||||
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"question": {
|
"question": {
|
||||||
|
|
|
@ -81,7 +81,84 @@
|
||||||
"layers": [
|
"layers": [
|
||||||
"picnic_table",
|
"picnic_table",
|
||||||
"bench",
|
"bench",
|
||||||
"bench_at_pt"
|
"bench_at_pt",
|
||||||
|
{
|
||||||
|
"builtin": "maproulette_challenge",
|
||||||
|
"override": {
|
||||||
|
"name=": {
|
||||||
|
"en": "Data from OpenBenches.org"
|
||||||
|
},
|
||||||
|
"minzoom": 10,
|
||||||
|
"source": {
|
||||||
|
"#": "Alternatively, if combined with `geojsonZoomLevel`, use geoJson with {y_min}, {y_max}, {x_min} and {x_max}. Only replace <id> in the next string: `https://maproulette.org/api/v2/challenge/view/<id>?bbox={x_min},{y_max},{x_max},{y_min}`",
|
||||||
|
"geoJsonZoomLevel": 12,
|
||||||
|
"idKey": "",
|
||||||
|
"geoJson": "https://maproulette.org/api/v2/challenge/view/53298?bbox={x_min},{y_max},{x_max},{y_min}"
|
||||||
|
},
|
||||||
|
"calculatedTags": [
|
||||||
|
"_id:=''+get(feat)('tags')['openbenches:id']",
|
||||||
|
"image:=get(feat)('tags')['image']",
|
||||||
|
"_osm_poi_with_this_ref=closestn(feat)('bench',25, undefined, 50).filter(f => f.feat.properties['openbenches:id'] === feat.properties['_id']).map(f => f.feat.properties.id).join(';')",
|
||||||
|
"_nearby_osm_poi=closestn(feat)('bench', 5, undefined, 15)",
|
||||||
|
"_nearby_osm_poi:count=get(feat)('_nearby_osm_poi')?.length",
|
||||||
|
"_nearby_osm_poi:props=get(feat)('_nearby_osm_poi')?.map(f => ({_distance: Math.round(f.distance), _mr_id: feat.properties.id, ...f.feat.properties}))"
|
||||||
|
],
|
||||||
|
"=tagRenderings": [
|
||||||
|
{
|
||||||
|
"id": "images",
|
||||||
|
"render": "<img src='{image}'/>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "closeness-indicator",
|
||||||
|
"condition": "_has_closeby_feature=yes",
|
||||||
|
"render": {
|
||||||
|
"en": "OpenStreetMap knows about <a href='#{_closest_osm_poi}'>a bench which is {_closest_osm_poi_distance} meter away.</a> "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "list_nearby_pois",
|
||||||
|
"condition": {
|
||||||
|
"and": [
|
||||||
|
"mr_taskStatus=Created",
|
||||||
|
"_nearby_osm_poi:count>0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"render": {
|
||||||
|
"before": {
|
||||||
|
"en": "Choose below which bench you want to link.",
|
||||||
|
"nl": "Kies hieronder welke bank je wilt linken."
|
||||||
|
},
|
||||||
|
"special": {
|
||||||
|
"classes": "p-2 m-1 my-4 border-2 border-dashed border-black",
|
||||||
|
"key": "_nearby_osm_poi:props",
|
||||||
|
"tagrendering": "<b><a href='#{id}'>{id}</a></b> ({_distance}m, {openbenches:id}) {minimap(17,id;_original:id)} {tag_apply($_original:tags,Link this object.,link,id,_original:id)}",
|
||||||
|
"type": "multi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "import_point",
|
||||||
|
"condition": "mr_taskStatus=Created",
|
||||||
|
"render": {
|
||||||
|
"special": {
|
||||||
|
"type": "import_button",
|
||||||
|
"targetLayer": "bench",
|
||||||
|
"tags": "$tags",
|
||||||
|
"text": {
|
||||||
|
"en": "Create a bench in OSM with the properties of openBenches.org",
|
||||||
|
"nl": "Maak een bank aan in OSM met de attributen van openBenches.org"
|
||||||
|
},
|
||||||
|
"to_point": "yes",
|
||||||
|
"maproulette_id": "mr_taskId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"maproulette.controls",
|
||||||
|
"all_tags"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"widenFactor": 1.5
|
"widenFactor": 1.5
|
||||||
}
|
}
|
||||||
|
|
230
scripts/importscripts/openbenches.ts
Normal file
230
scripts/importscripts/openbenches.ts
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
import Script from "../Script"
|
||||||
|
import { promises as fs, writeFileSync } from "fs"
|
||||||
|
import { Feature, Point } from "geojson"
|
||||||
|
import { join } from "path"
|
||||||
|
import sqlite3, { Database } from "sqlite3"
|
||||||
|
import { open } from "sqlite"
|
||||||
|
import ScriptUtils from "../ScriptUtils"
|
||||||
|
import { Lists } from "../../src/Utils/Lists"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note:
|
||||||
|
* npm i sqlite sqlite3
|
||||||
|
* I didn't want this into the deps
|
||||||
|
* "sqlite": "^5.1.1",
|
||||||
|
* "sqlite3": "^5.1.7",
|
||||||
|
*/
|
||||||
|
interface Bench {
|
||||||
|
benchID: number,
|
||||||
|
latitude: number,
|
||||||
|
longitude: number,
|
||||||
|
address: string,
|
||||||
|
inscription: string,
|
||||||
|
description: string,
|
||||||
|
present: 0 | 1,
|
||||||
|
published: 0 | 1,
|
||||||
|
added: string,
|
||||||
|
userID: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
name: string,
|
||||||
|
providerID: string,
|
||||||
|
provider: string,
|
||||||
|
userID: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tag {
|
||||||
|
tagID: number,
|
||||||
|
tagText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function mediaUrl(sha: string | { "sha1": string }): string {
|
||||||
|
if (sha["sha1"]) {
|
||||||
|
sha = sha["sha1"]
|
||||||
|
}
|
||||||
|
return `https://openbenches.org/image/${sha}.jpg`
|
||||||
|
}
|
||||||
|
|
||||||
|
class Openbenches extends Script {
|
||||||
|
private db: Database
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super("Creates the OpenBenches dataset to upload to maproulette")
|
||||||
|
}
|
||||||
|
|
||||||
|
async buildDatabase(sqlDir: string, dbFile: string) {
|
||||||
|
const db = await open({
|
||||||
|
filename: dbFile,
|
||||||
|
driver: sqlite3.Database,
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = await fs.readdir(sqlDir)
|
||||||
|
const sqlFiles = files.filter(f => f.endsWith(".sql"))
|
||||||
|
|
||||||
|
const skip = ["database.sql"]
|
||||||
|
|
||||||
|
|
||||||
|
const order = ["tags", "users", "tag_map", "media_types", "benches", "media"]
|
||||||
|
|
||||||
|
for (let file of order) {
|
||||||
|
console.log("Exec file", file)
|
||||||
|
file = "openbenc_benches_table_" + file + ".sql"
|
||||||
|
let content = await fs.readFile(join(sqlDir, file), "utf-8")
|
||||||
|
content = content.replaceAll("ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", "")
|
||||||
|
.replaceAll("\\'", "''")
|
||||||
|
await db.exec(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.close()
|
||||||
|
console.log("DB has been seeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
all<T>(query): Promise<T[]> {
|
||||||
|
return new Promise<T[]>((resolve, reject) => {
|
||||||
|
this.db.all(query, (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(<any>rows)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadDb(dbFile: string): Promise<Database> {
|
||||||
|
const db = await open({
|
||||||
|
filename: dbFile,
|
||||||
|
driver: sqlite3.Database,
|
||||||
|
})
|
||||||
|
console.log(db)
|
||||||
|
console.dir(db)
|
||||||
|
return <any>db.db
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBenchInfo(benchWithUser: Bench & User, tags: string[]): Promise<Feature<Point>> {
|
||||||
|
const id = benchWithUser.benchID
|
||||||
|
const media = await this.all<{
|
||||||
|
sha1: string,
|
||||||
|
media_type: string
|
||||||
|
}>("SELECT * FROM media WHERE media.benchID = " + id)
|
||||||
|
const mediaBench = media.filter(m => m.media_type === "bench")
|
||||||
|
const mediaInscr = media.filter(m => m.media_type === "inscription")
|
||||||
|
const mediaView = media.filter(m => m.media_type === "view")
|
||||||
|
|
||||||
|
const properties = {
|
||||||
|
// added: benchWithUser.added,
|
||||||
|
"openbenches:id": id,
|
||||||
|
inscription: benchWithUser.inscription.replaceAll("\\r\\n", "\n"),
|
||||||
|
amenity: "bench",
|
||||||
|
// lastModifiedBy: benchWithUser.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mediaMerged = Lists.dedup(mediaBench.concat(mediaInscr).map(m => mediaUrl(m)))
|
||||||
|
for (let i = 0; i < mediaMerged.length; i++) {
|
||||||
|
const m = mediaMerged[i]
|
||||||
|
if (i === 0) {
|
||||||
|
properties["image"] = m
|
||||||
|
} else {
|
||||||
|
properties["image:" + (i - 1)] = m
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < mediaView.length; i++) {
|
||||||
|
const m = mediaView[i]
|
||||||
|
if (i === 0) {
|
||||||
|
properties["image:view"] = mediaUrl(m)
|
||||||
|
} else {
|
||||||
|
properties["image:view:" + (i - 1)] = mediaUrl(m)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagsToProperties = {
|
||||||
|
"wooden": "material=wood",
|
||||||
|
"metal": "material=metal",
|
||||||
|
"indoors": "indoor=yes",
|
||||||
|
"stone": "material=stone",
|
||||||
|
"poem": "artwork=poem",
|
||||||
|
"statue": "artwork=statue",
|
||||||
|
"composite": "material=plastic",
|
||||||
|
/*"cat":"subject=cat",
|
||||||
|
"dog":"subject=dog" Not always a pet, sometimes also a 'dogwalker', someone mentioning their cat, ... */
|
||||||
|
// EMOJI: very broad category, basically that a little image is part of the 'inscription'. Should be handled by adding the emoji directly
|
||||||
|
// Twinned: basically, two people are remembered, often a couple -> inscription and/or subject handles this
|
||||||
|
// Picture: plaque has a little picture -> subset of plaque
|
||||||
|
// Famous: someone "famous" is remembered, although I don't know half of 'm. Too subjective for OSM
|
||||||
|
// FUnny: talk about subjective...
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tag of (tags ?? [])) {
|
||||||
|
const match = tagsToProperties[tag]
|
||||||
|
if (!match) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const [k, v] = match.split("=")
|
||||||
|
properties[k] = v
|
||||||
|
tags.splice(tags.indexOf(tag), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "Feature",
|
||||||
|
properties: { tags: JSON.stringify(properties) },
|
||||||
|
geometry: {
|
||||||
|
type: "Point",
|
||||||
|
coordinates: [benchWithUser.longitude, benchWithUser.latitude],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async main(args: string[]): Promise<void> {
|
||||||
|
const dbFile = "openbenches.sqlite"
|
||||||
|
let createTest = false
|
||||||
|
|
||||||
|
// rmSync(dbFile)
|
||||||
|
// await this.buildDatabase("/home/pietervdvn/git/openbenches.org/database", dbFile)
|
||||||
|
this.db = await this.loadDb(dbFile)
|
||||||
|
|
||||||
|
const tags = new Map<number, string>()
|
||||||
|
const tagRows = await this.all<Tag>("SELECT * FROM tags")
|
||||||
|
for (const tag of tagRows) {
|
||||||
|
tags.set(tag.tagID, tag.tagText)
|
||||||
|
}
|
||||||
|
const tagsOnBenches = new Map<number, string[]>()
|
||||||
|
const tagOnBench = await this.all<{ benchID: number, tagID: number }>("SELECT * from tag_map")
|
||||||
|
for (const tg of tagOnBench) {
|
||||||
|
const bench = tg.benchID
|
||||||
|
if (!tagsOnBenches.has(bench)) {
|
||||||
|
tagsOnBenches.set(bench, [])
|
||||||
|
}
|
||||||
|
tagsOnBenches.get(bench).push(tags.get(tg.tagID))
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = await this.all<Bench & User>("SELECT * FROM benches INNER JOIN users ON benches.userID = users.userID")
|
||||||
|
const features: Feature<Point>[] = []
|
||||||
|
for (let i = 0; i < r.length; i++) {
|
||||||
|
const benchWithUser = r[i]
|
||||||
|
if (benchWithUser.present === 0 || benchWithUser.published === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const tags = tagsOnBenches.get(benchWithUser.benchID)
|
||||||
|
if (i % 100 === 0) {
|
||||||
|
ScriptUtils.erasableLog(`Processing bench ${i}/${r.length} (${Math.round(100 * i / r.length)}%) `)
|
||||||
|
}
|
||||||
|
features.push(await this.createBenchInfo(benchWithUser, tags))
|
||||||
|
if (createTest && features.length > 1000) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(`openbenches_export${createTest ? "_test" : ""}.geojson`, JSON.stringify({
|
||||||
|
type: "FeatureCollection", features,
|
||||||
|
}, null, " "), "utf-8")
|
||||||
|
//console.log(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Openbenches().run()
|
|
@ -471,7 +471,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
||||||
|
|
||||||
private static escapeStr(v: string, context: ConversionContext): string {
|
private static escapeStr(v: string, context: ConversionContext): string {
|
||||||
if (typeof v !== "string") {
|
if (typeof v !== "string") {
|
||||||
context.err("Detected a non-string value where one expected a string: " + v)
|
context.err("Detected a non-string value where one expected a string (while rewriting a special): " + JSON.stringify(v))
|
||||||
return RewriteSpecial.escapeStr("" + v, context)
|
return RewriteSpecial.escapeStr("" + v, context)
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
|
|
|
@ -72,7 +72,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
||||||
.enters("source", "osmTags")
|
.enters("source", "osmTags")
|
||||||
.err(
|
.err(
|
||||||
"The tags that will be used to load data from OpenStreetMap are all negative - this means that they all match something that _doesn't_ have a certain tag. For example, `key=` means anything without `key`. Did you perhaps mean to use `key~*`, meaning anything _with_ this key set? The tags are:\n\t" +
|
"The tags that will be used to load data from OpenStreetMap are all negative - this means that they all match something that _doesn't_ have a certain tag. For example, `key=` means anything without `key`. Did you perhaps mean to use `key~*`, meaning anything _with_ this key set? The tags are:\n\t" +
|
||||||
osmTags.asHumanString(false, false, {})
|
osmTags.asHumanString(false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,6 +156,9 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
||||||
context.err("Layer " + json.id + " does not have an explicit 'allowMove'")
|
context.err("Layer " + json.id + " does not have an explicit 'allowMove'")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(json.source["geojsonZoomLevel"]){
|
||||||
|
context.enter("source").err("Use geoJsonZoomLevel (with capital J) instead of geojsonZoomLevel")
|
||||||
|
}
|
||||||
|
|
||||||
if (context.hasErrors()) {
|
if (context.hasErrors()) {
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -408,7 +411,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
||||||
"This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
|
"This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
|
||||||
tags.asHumanString(false, false, {}) +
|
tags.asHumanString(false, false, {}) +
|
||||||
"\n The required tags are: " +
|
"\n The required tags are: " +
|
||||||
baseTags.asHumanString(false, false, {})
|
baseTags.asHumanString(false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,15 +17,11 @@
|
||||||
export let idkeys: string[]
|
export let idkeys: string[]
|
||||||
export let feature: Feature
|
export let feature: Feature
|
||||||
export let clss: string = "h-40 rounded"
|
export let clss: string = "h-40 rounded"
|
||||||
const keys = idkeys
|
let featuresToShow: Store<Feature<Geometry, OsmTags>[]> = state.indexedFeatures.featuresById.mapD(
|
||||||
let featuresToShow: Store<Feature<Geometry, OsmTags>[]> = state.indexedFeatures.featuresById.map(
|
(featuresById: Map<string, Feature>) => {
|
||||||
(featuresById) => {
|
|
||||||
if (featuresById === undefined) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const properties = tags.data
|
const properties = tags.data
|
||||||
const features: Feature<Geometry, OsmTags>[] = []
|
const features: Feature<Geometry, OsmTags>[] = []
|
||||||
for (const key of keys) {
|
for (const key of idkeys) {
|
||||||
const value = properties[key]
|
const value = properties[key]
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
continue
|
continue
|
||||||
|
@ -59,7 +55,7 @@
|
||||||
let mla = new MapLibreAdaptor(mlmap, {
|
let mla = new MapLibreAdaptor(mlmap, {
|
||||||
rasterLayer: state.mapProperties.rasterLayer,
|
rasterLayer: state.mapProperties.rasterLayer,
|
||||||
zoom: new UIEventSource<number>(17),
|
zoom: new UIEventSource<number>(17),
|
||||||
maxzoom: new UIEventSource<number>(17),
|
maxzoom: new UIEventSource<number>(22),
|
||||||
rotation: state.mapProperties.rotation.followingClone(),
|
rotation: state.mapProperties.rotation.followingClone(),
|
||||||
allowRotating: state.mapProperties.allowRotating.followingClone(),
|
allowRotating: state.mapProperties.allowRotating.followingClone(),
|
||||||
})
|
})
|
||||||
|
|
|
@ -22,10 +22,12 @@
|
||||||
export let onApply: () => Promise<void>
|
export let onApply: () => Promise<void>
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
const t = Translations.t.general.apply_button
|
const t = Translations.t.general.apply_button
|
||||||
|
export let maprouletteIdKey: string
|
||||||
|
|
||||||
|
|
||||||
// THis button might be shown on MapRoulette-items, which might already have been applied
|
// THis button might be shown on MapRoulette-items, which might already have been applied
|
||||||
// This will default to 'false' for non-maproulette challenges
|
// This will default to 'false' for non-maproulette challenges
|
||||||
let isMaprouletteAndApplied =
|
let isMaprouletteAndApplied = tags?.data?.["mr_taskStatus"] !== undefined && tags?.data?.["mr_taskStatus"] !== "Created"
|
||||||
tags?.data?.["mr_taskStatus"] !== undefined && tags?.data?.["mr_taskStatus"] !== "Created"
|
|
||||||
|
|
||||||
let currentState: UIEventSource<"init" | "applying" | "applied"> = new UIEventSource(
|
let currentState: UIEventSource<"init" | "applying" | "applied"> = new UIEventSource(
|
||||||
isMaprouletteAndApplied ? "applied" : "init"
|
isMaprouletteAndApplied ? "applied" : "init"
|
||||||
|
@ -33,15 +35,17 @@
|
||||||
|
|
||||||
async function apply() {
|
async function apply() {
|
||||||
currentState.set("applying")
|
currentState.set("applying")
|
||||||
await onApply()
|
window.requestIdleCallback(async () => {
|
||||||
currentState.set("applied")
|
await onApply()
|
||||||
|
currentState.set("applied")
|
||||||
|
}, {timeout: 2000})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoginToggle {state} ignoreLoading>
|
<LoginToggle {state} ignoreLoading>
|
||||||
{#if $currentState === "init"}
|
{#if $currentState === "init"}
|
||||||
<button on:click={() => apply()}>
|
<button on:click={() => apply()}>
|
||||||
<Icon icon={image} />
|
<Icon icon={image} clss="w-8" />
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>{msg}</div>
|
<div>{msg}</div>
|
||||||
{#if targetIdKey}
|
{#if targetIdKey}
|
||||||
|
@ -55,6 +59,11 @@
|
||||||
{:else}
|
{:else}
|
||||||
<TagHint tags={$tagsToApply} />
|
<TagHint tags={$tagsToApply} />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if maprouletteIdKey}
|
||||||
|
<div class="subtle">
|
||||||
|
Closes maproulette {$tags[maprouletteIdKey]}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{:else if $currentState === "applying"}
|
{:else if $currentState === "applying"}
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default class TagApplyViz extends SpecialVisualization implements AutoAct
|
||||||
public readonly args = [
|
public readonly args = [
|
||||||
{
|
{
|
||||||
name: "tags_to_apply",
|
name: "tags_to_apply",
|
||||||
doc: "A specification of the tags to apply. This is either hardcoded in the layer or the `$name` of a property containing the tags to apply. If redirected and the value of the linked property starts with `{`, the other property will be interpreted as a json object",
|
doc: "A specification of the tags to apply. This is either hardcoded in the layer or the `$name` of a property containing the tags to apply. If redirected and the value of the linked property starts with `{`, this value will be interpreted as a json object. All the keys and values will be applied from this object",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "message",
|
name: "message",
|
||||||
|
@ -68,7 +68,7 @@ export default class TagApplyViz extends SpecialVisualization implements AutoAct
|
||||||
const properties = JSON.parse(spec)
|
const properties = JSON.parse(spec)
|
||||||
tgsSpec = []
|
tgsSpec = []
|
||||||
for (const key of Object.keys(properties)) {
|
for (const key of Object.keys(properties)) {
|
||||||
tgsSpec.push([key, properties[key]])
|
tgsSpec.push([key, ""+ properties[key]])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tgsSpec = TagApplyViz.parseTagSpec(spec)
|
tgsSpec = TagApplyViz.parseTagSpec(spec)
|
||||||
|
@ -182,10 +182,11 @@ export default class TagApplyViz extends SpecialVisualization implements AutoAct
|
||||||
const tagsToApply: Store<Tag[]> = TagApplyViz.generateTagsToApply(args[0], tags)
|
const tagsToApply: Store<Tag[]> = TagApplyViz.generateTagsToApply(args[0], tags)
|
||||||
const msg = args[1]
|
const msg = args[1]
|
||||||
let image = args[2]?.trim()
|
let image = args[2]?.trim()
|
||||||
|
const targetIdKey = args[3]
|
||||||
|
const maprouletteId = args[4]
|
||||||
if (image === "" || image === "undefined") {
|
if (image === "" || image === "undefined") {
|
||||||
image = undefined
|
image = undefined
|
||||||
}
|
}
|
||||||
const targetIdKey = args[3]
|
|
||||||
|
|
||||||
const onApply = async () => {
|
const onApply = async () => {
|
||||||
await this.applyActionOn(feature, state, tags, args)
|
await this.applyActionOn(feature, state, tags, args)
|
||||||
|
@ -199,6 +200,7 @@ export default class TagApplyViz extends SpecialVisualization implements AutoAct
|
||||||
image,
|
image,
|
||||||
targetIdKey,
|
targetIdKey,
|
||||||
onApply,
|
onApply,
|
||||||
|
maprouletteIdKey: maprouletteId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ class Multi extends SpecialVisualization {
|
||||||
funcName = "multi"
|
funcName = "multi"
|
||||||
group = "tagrendering_manipulation"
|
group = "tagrendering_manipulation"
|
||||||
docs =
|
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"
|
"Given an embedded tagRendering (read only) and a key, will read `properties[key]` as a JSON-list. Every element of this list will be considered as the main 'tags' and rendered with the tagRendering. The original tags are available as _original:<key>"
|
||||||
example =
|
example =
|
||||||
"```json\n" +
|
"```json\n" +
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
|
@ -139,13 +139,13 @@ class Multi extends SpecialVisualization {
|
||||||
const translation = new Translation({ "*": tr })
|
const translation = new Translation({ "*": tr })
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
tags.map((tags) => {
|
tags.map((tags) => {
|
||||||
let properties: object[]
|
let propertiess: object[]
|
||||||
if (typeof tags[key] === "string") {
|
if (typeof tags[key] === "string") {
|
||||||
properties = JSON.parse(tags[key])
|
propertiess = JSON.parse(tags[key])
|
||||||
} else {
|
} else {
|
||||||
properties = <object[]>(<unknown>tags[key])
|
propertiess = <object[]>(<unknown>tags[key])
|
||||||
}
|
}
|
||||||
if (!properties) {
|
if (!propertiess) {
|
||||||
console.debug(
|
console.debug(
|
||||||
"Could not create a special visualization for multi(",
|
"Could not create a special visualization for multi(",
|
||||||
args.join(", ") + ")",
|
args.join(", ") + ")",
|
||||||
|
@ -155,10 +155,15 @@ class Multi extends SpecialVisualization {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const elements = []
|
const elements = []
|
||||||
for (const property of properties) {
|
|
||||||
|
for (const properties of propertiess) {
|
||||||
|
const propertiesWithOriginal = {...properties}
|
||||||
|
for (const k in tags) {
|
||||||
|
propertiesWithOriginal["_original:"+k]=tags[k]
|
||||||
|
}
|
||||||
const subsTr = new SvelteUIElement(SpecialTranslation, {
|
const subsTr = new SvelteUIElement(SpecialTranslation, {
|
||||||
t: translation,
|
t: translation,
|
||||||
tags: new ImmutableStore(property),
|
tags: new ImmutableStore(propertiesWithOriginal),
|
||||||
state,
|
state,
|
||||||
feature,
|
feature,
|
||||||
layer,
|
layer,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue