Themes(benches): more work on integrating openbenches, polish a bit more

This commit is contained in:
Pieter Vander Vennet 2025-10-12 01:51:20 +02:00
parent 87823caabc
commit 11ab4965c6
6 changed files with 212 additions and 221 deletions

View file

@ -12,7 +12,35 @@ A perfect example of this is to setup such a challenge to e.g. import new points
## Preparing the data
Convert your source data into a geojson. Use *`tags`* as field where all the OSM-properties should go. Make sure to include all tags there.
Convert your source data into a geojson. Use *`tags`* as field where all the OSM-properties should go. Make sure to include all tags that should be imported there and stringify it.
You have quite some freedom for the other tags. Make sure to add an *id* tag. Other tags (e.g. images, metadata) may go into the properties as well.
However, do _not_ add the generic tags for the layer to import into, to avoid that the maproulette challenges are loaded by the target layer.
```
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [1.234, 5.678]},
"properties": {
"id": ..., # Especially important when you want to rebuild the data
"image": ...,
"lastModifiedBy": ..., # from the source data, just to help
"tags": "foo=bar;name=xyz;image=...", # To be imported
}
}
]
}
```
Then, create a challenge on https://maproulette.org/admin/projects (or, if you want to test first, on https://staging.maproulette.org/)
Hint: MapRoulette has a button 'rebuild task', where you can first 'remove all incomplete tasks'. This is perfect to start over in case of small data errors.
@ -127,35 +155,3 @@ The following example uses the calculated tags `_has_closeby_feature` and `_clos
The easiest way is to reuse a tagrendering from the [Maproulette-layer](./Docs/Layers/maproulette.md) (_not_ the `maproulette-challenge`-layer!), such as [`maproulette.mark_fixed`](./Docs/Layers/maproulette.md#markfixed),[`maproulette.mark_duplicate`](./Docs/Layers/maproulette.md#markduplicate),[`maproulette.mark_too_hard`](./Docs/Layers/maproulette.md#marktoohard).
In the background, these use the special visualisation [`maproulette_set_status`](./Docs/SpecialRenderings.md#maproulettesetstatus) - which allows to apply different status codes or different messages/icons.
## Creating a maproulette challenge
A challenge can be created on https://maproulette.org/admin/projects
This can be done with a geojson-file (or by other means).
MapRoulette works as a geojson-store with status fields added. As such, you have a bit of freedom in creating the data, but an **id** field is mandatory. A **name** tag is recommended
To setup a guided import, add a `tags`-field with tags formatted in such a way that they are compatible with the [import-button](./Docs/SpecialRenderings.md#specifying-which-tags-to-copy-or-add)
(The following example is not tested and might be wrong.)
```
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [1.234, 5.678]},
"properties": {
"id": ...
"tags": "foo=bar;name=xyz",
}
}
]
}
```

View file

@ -107,16 +107,15 @@
"=tagRenderings": [
{
"id": "explanation",
"classes":"border-2 border-interactive low-interaction p-2 m-2",
"render": {
"en": "This is a bench that is known in Openbenches.org but might not exist in OpenStreetMap. If this bench still exists in the real world, you can add or link this information to OpenStreetMap with the tools below"
}
},
{
"id": "images",
"render": "<img src='{image}'/>"
},
{
"id": "see_on_openbenches",
"classes": "text-lg button",
"icon": "./assets/themes/benches/openbencheslogo.svg",
"render": {
"special": {
"type": "link",
@ -127,6 +126,10 @@
}
}
},
{
"id": "images",
"render": "{image_carousel()}"
},
{
"id": "closeness-indicator",
"condition": "_has_closeby_feature=yes",

View file

@ -20,5 +20,15 @@
"https://github.com/streetcomplete/StreetComplete/blob/v25.0-beta1/res/graphics/authors.txt",
"https://github.com/streetcomplete/StreetComplete/tree/v25.0-beta1/res/graphics/quest%20icons"
]
},
{
"path": "openbencheslogo.svg",
"license": "LOGO",
"authors": [
"Terence Eden"
],
"sources": [
"https://openbenches.org/images/openbencheslogo.svg"
]
}
]

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800">
<circle cx="400" cy="400" r="400" fill="#11b0a3"/>
<circle cx="573" cy="275" r="106" fill="#070"/>
<path d="M153 345h256v77H153z" fill="#fff"/>
<path d="M191 376h177v20H191z" fill="#ffba00"/>
<path d="M28.7 507.6h735.7C706.5 748.5 539.5 771.1 409.2 792c-181.7-27.8-324-103.8-380.5-284.4z" fill="#8c5015"/>
<path d="M607 490c9-29 66 4 161-112 29-7 15 79 8 111z" fill="#070"/>
<path d="M259 197l19-7c-1 0-10-36 17-50l-10-18a55 55 0 0 0-29 45 95 95 0 0 0-49-10l2 20s39-3 50 20zM326 256l18 9c1-2 19-33 48-24l5-19c-21-7-39 0-52 11-6-24-26-41-27-42l-13 15c0 1 29 26 21 50z" fill="#000"/>
<path d="M768.6 244.3a398.7 398.7 0 0 0-85.8-127.1A398.7 398.7 0 0 0 400 0a397.5 397.5 0 0 0-282.8 117.2A398.7 398.7 0 0 0 0 400a397.5 397.5 0 0 0 117.2 282.8A398.7 398.7 0 0 0 400 800a397.5 397.5 0 0 0 282.8-117.2A398.7 398.7 0 0 0 800 400c0-54-10.6-106.4-31.4-155.7zM30.3 487.6A380.4 380.4 0 0 1 400 20c197.2 0 359.7 151 378.2 343.4a69.2 69.2 0 0 0-56.6 57.7 71 71 0 0 0-53.8 37.2 48.7 48.7 0 0 0-66.1 29.1h-20.5V381.8a110.5 110.5 0 0 0-12-220.2 110.5 110.5 0 0 0-8.1 220.5v105.5H396.5v-47h72.3v-20h-40.5v-94.4H133.5v94.4h-32.1v20h64.2v47zm644 0h-50.6a28.5 28.5 0 0 1 42.4-6.9zM561.1 324.4V362a90.5 90.5 0 0 1 8-180.3 90.4 90.4 0 0 1 12 179.9v-29.2l56.4-56.3-14.2-14.2-42.1 42.1v-38H561v30l-37-37.1-14.3 14.2zm-407.5 96.2v-74.3h254.6v74.3zm222.8 20v47H185.7v-47zM400 780c-41.2 0-81-6.6-118.1-18.8l403.8-111A379.1 379.1 0 0 1 400 780zm307-156.4L250 749c-11.4-5-22.6-10.4-33.4-16.4L484 655l-5.5-19.3-285.3 82.8c-13.3-8.7-26-18.2-38.1-28.4l131.8-24.9-3.7-19.7-146.8 27.7a383.3 383.3 0 0 1-36-39.8l637.6-60c-9 17.5-19.4 34.2-31 50zm41-71.2L86.6 614.6a378.6 378.6 0 0 1-51-107h728.8c-4.5 15.4-10 30.3-16.4 44.7zm-69.7-64.7L682 476a51 51 0 0 1 48.6-35.4h10l.2-9.8a49.3 49.3 0 0 1 38.8-47.3 385.6 385.6 0 0 1-9.9 104z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Terence Eden
SPDX-License-Identifier: LicenseRef-LOGO

View file

@ -25,156 +25,93 @@ import { GeoOperations } from "../../src/Logic/GeoOperations"
* "sqlite3": "^5.1.7",
*/
interface Bench {
benchID: number,
latitude: number,
longitude: number,
address: string,
inscription: string,
description: string,
present: 0 | 1,
published: 0 | 1,
benchID: number
latitude: number
longitude: number
address: string
inscription: string
description: string
present: 0 | 1
published: 0 | 1
/* time of creation (or possibly last edit?) */
added: string,
added: string
userID: number
}
interface User {
name: string,
providerID: string,
provider: string,
name: string
providerID: string
provider: string
userID: number
}
interface Tag {
tagID: number,
tagID: number
tagText: string
}
function mediaUrl(sha: string | { "sha1": string }): string {
function mediaUrl(sha: string | { sha1: string }): string {
if (sha["sha1"]) {
sha = sha["sha1"]
}
return `https://openbenches.org/image/${sha}.jpg`
}
const uk: Feature<Polygon> = {
"type": "Feature",
"properties": {},
"geometry": {
"coordinates": [
const uk: Feature<Polygon> = {
type: "Feature",
properties: {},
geometry: {
coordinates: [
[
[
3.139397666817615,
53.112746745001914
],
[
0.12546232547020963,
61.34289409315957
],
[
-5.193638926198332,
60.3858935023425
],
[
-12.316831332595541,
56.76308878364702
],
[
-12.586640816376246,
51.076733390490034
],
[
-3.6443836396576046,
49.4256703574342
],
[
1.0194660085441853,
50.442813369706585
],
[
3.139397666817615,
53.112746745001914
]
]
[3.139397666817615, 53.112746745001914],
[0.12546232547020963, 61.34289409315957],
[-5.193638926198332, 60.3858935023425],
[-12.316831332595541, 56.76308878364702],
[-12.586640816376246, 51.076733390490034],
[-3.6443836396576046, 49.4256703574342],
[1.0194660085441853, 50.442813369706585],
[3.139397666817615, 53.112746745001914],
],
],
"type": "Polygon"
}
type: "Polygon",
},
}
const us : Feature<Polygon> = {
"type": "Feature",
"properties": {},
"geometry": {
"coordinates": [
const us: Feature<Polygon> = {
type: "Feature",
properties: {},
geometry: {
coordinates: [
[
[
-171.55472370762342,
71.44263911390138
],
[
-171.31347027402668,
33.24735774004321
],
[
-105.9804086342826,
-3.5292610992716362
],
[
-57.00596161415962,
15.805666337324794
],
[
-32.880618254493015,
49.584578264365916
],
[
-47.35582427029317,
72.85409976292118
],
[
-101.60890406091582,
79.0557752859543
],
[
-171.55472370762342,
71.44263911390138
]
]
[-171.55472370762342, 71.44263911390138],
[-171.31347027402668, 33.24735774004321],
[-105.9804086342826, -3.5292610992716362],
[-57.00596161415962, 15.805666337324794],
[-32.880618254493015, 49.584578264365916],
[-47.35582427029317, 72.85409976292118],
[-101.60890406091582, 79.0557752859543],
[-171.55472370762342, 71.44263911390138],
],
],
"type": "Polygon"
}
type: "Polygon",
},
}
const australia: Feature<Polygon> = {
"type": "Feature",
"properties": {},
"geometry": {
"coordinates": [
const australia: Feature<Polygon> = {
type: "Feature",
properties: {},
geometry: {
coordinates: [
[
[
177.6309142850211,
-48.72845301037672
],
[
177.6309142850211,
-8.050870320392335
],
[
107.59695622498174,
-8.050870320392335
],
[
107.59695622498174,
-48.72845301037672
],
[
177.6309142850211,
-48.72845301037672
]
]
[177.6309142850211, -48.72845301037672],
[177.6309142850211, -8.050870320392335],
[107.59695622498174, -8.050870320392335],
[107.59695622498174, -48.72845301037672],
[177.6309142850211, -48.72845301037672],
],
],
"type": "Polygon"
}
type: "Polygon",
},
}
const areas = {uk, us, australia}
const areas = { uk, us, australia }
class Openbenches extends Script {
private db: Database
@ -190,18 +127,18 @@ class Openbenches extends Script {
})
const files = await fs.readdir(sqlDir)
const sqlFiles = files.filter(f => f.endsWith(".sql"))
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", "")
content = content
.replaceAll("ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", "")
.replaceAll("\\'", "''")
await db.exec(content)
}
@ -228,39 +165,37 @@ class Openbenches extends Script {
driver: sqlite3.Database,
})
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,
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 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 inscription = benchWithUser.inscription.replaceAll("\\r\\n", "\n")
const properties = {
lastModifiedTime: benchWithUser.added,
"openbenches:id": id,
inscription: inscription.slice(0,255),
inscription: inscription.slice(0, 255),
amenity: "bench",
lastModifiedBy: benchWithUser.name,
}
if(inscription.length >= 255){
if (inscription.length >= 255) {
properties["inscription:0"] = inscription.slice(255)
}
let mediaMerged = Lists.dedup(mediaBench.concat(mediaInscr).map(m => mediaUrl(m)))
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
}
}
@ -270,18 +205,17 @@ class Openbenches extends Script {
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",
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
@ -291,7 +225,7 @@ class Openbenches extends Script {
// FUnny: talk about subjective...
}
for (const tag of (tags ?? [])) {
for (const tag of tags ?? []) {
const match = tagsToProperties[tag]
if (!match) {
continue
@ -314,7 +248,10 @@ class Openbenches extends Script {
async getAlreadyImported(): Promise<FeatureCollection> {
const alreadyImportedPath = "openbenches_linked_in_osm.geojson"
if (!existsSync(alreadyImportedPath)) {
const overpass = new Overpass(Constants.defaultOverpassUrls[0], TagUtils.Tag("openbenches:id~*"))
const overpass = new Overpass(
Constants.defaultOverpassUrls[0],
TagUtils.Tag("openbenches:id~*")
)
const dataAndDate = await overpass.queryGeoJson(BBox.global)
const data = dataAndDate[0]
writeFileSync(alreadyImportedPath, JSON.stringify(data), "utf-8")
@ -345,7 +282,11 @@ class Openbenches extends Script {
}
if (key.startsWith("image")) {
const imgValue = ob.properties[key]
if (Object.values(bench.properties).some(v => v === imgValue || (v + ".jpg") === imgValue)) {
if (
Object.values(bench.properties).some(
(v) => v === imgValue || v + ".jpg" === imgValue
)
) {
continue
}
let ikey = "image"
@ -354,37 +295,44 @@ class Openbenches extends Script {
i++
ikey = "image:" + i
}
const li = new ChangeTagAction(bench.properties.id, new OsmTag(ikey, imgValue), bench.properties, {
theme: "openbenches",
changeType: "link-image",
})
const li = new ChangeTagAction(
bench.properties.id,
new OsmTag(ikey, imgValue),
bench.properties,
{
theme: "openbenches",
changeType: "link-image",
}
)
changes.push(li)
bench.properties[ikey] = imgValue
console.log(` + ${ikey}=${imgValue}`)
} else if (!bench.properties[key]) {
const v = ob.properties[key]
if(v.length >= 255){
console.log("Text too long:", v.replaceAll("\n"," "))
if (v.length >= 255) {
console.log("Text too long:", v.replaceAll("\n", " "))
continue
}
changes.push(new ChangeTagAction(
bench.properties.id,
new OsmTag(key, v),
bench.properties,
{
theme: "openbenches",
changeType: "answer",
},
))
changes.push(
new ChangeTagAction(
bench.properties.id,
new OsmTag(key, v),
bench.properties,
{
theme: "openbenches",
changeType: "answer",
}
)
)
console.log(` - ${key}=${ob.properties[key].replaceAll("\n", " ")}`)
}
}
}
if(changes.length === 0){
if (changes.length === 0) {
return
}
const xml = await Changes.createChangesetXMLForJosm(changes)
writeFileSync(`attributes_import${area}.osc`,xml, "utf-8")
writeFileSync(`attributes_import${area}.osc`, xml, "utf-8")
}
async main(args: string[]): Promise<void> {
@ -394,12 +342,13 @@ class Openbenches extends Script {
const osmData = await this.getAlreadyImported()
// rmSync(dbFile)
if(!existsSync(dbFile)){
console.log("No database file found at "+dbFile+", recreating the database")
if (!existsSync(dbFile)) {
console.log("No database file found at " + dbFile + ", recreating the database")
await this.buildDatabase("/home/pietervdvn/git/openbenches.org/database", dbFile)
}
const alreadyLinked: Set<number> = new Set(osmData.features.map(f => Number(f.properties["openbenches:id"])))
const alreadyLinked: Set<number> = new Set(
osmData.features.map((f) => Number(f.properties["openbenches:id"]))
)
this.db = await this.loadDb(dbFile)
@ -409,7 +358,9 @@ class Openbenches extends Script {
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")
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)) {
@ -418,11 +369,13 @@ class Openbenches extends Script {
tagsOnBenches.get(bench).push(tags.get(tg.tagID))
}
const openbenches = await this.all<Bench & User>("SELECT * FROM benches INNER JOIN users ON benches.userID = users.userID")
const openbenches = await this.all<Bench & User>(
"SELECT * FROM benches INNER JOIN users ON benches.userID = users.userID"
)
const features: Feature<Point>[] = []
let skipped = 0
for (let i = 0; i < openbenches.length; i++) {
if(alreadyLinked.has(i)){
if (alreadyLinked.has(i)) {
skipped++
continue
}
@ -432,33 +385,50 @@ class Openbenches extends Script {
}
const tags = tagsOnBenches.get(benchWithUser.benchID)
if (i % 100 === 0) {
ScriptUtils.erasableLog(`Processing bench ${i}/${openbenches.length} (${Math.round(100 * i / openbenches.length)}%) `)
ScriptUtils.erasableLog(
`Processing bench ${i}/${openbenches.length} (${Math.round(
(100 * i) / openbenches.length
)}%) `
)
}
features.push(await this.createBenchInfo(benchWithUser, tags))
if (createTest && features.length > 1000) {
break
}
}
/*
/*
writeFileSync(`openbenches_export_josm_${createTest ? "_test" : ""}.geojson`, JSON.stringify({
type: "FeatureCollection", features,
}, null, " "), "utf-8")*/
const maproulette = features
.map(f => {
const properties = {tags: JSON.stringify(f.properties)}
properties["id"] = "openbenches/"+f.properties["openbenches:id"]
return {...f, properties}
const maproulette = features.map((f) => {
const fProps = {
...f.properties
}
delete fProps["lastModifiedBy"]
delete fProps["lastModifiedTime"]
const properties = { ...f.properties, tags: JSON.stringify(fProps) }
delete properties["amenity"] // Makes sure mapcomplete doesn't think this is a bench...
properties["id"] = "openbenches/" + f.properties["openbenches:id"]
return { ...f, properties }
})
console.log("Skipped",skipped,"benches as already linked/imported")
writeFileSync(`openbenches_export_maproulette${createTest ? "_test" : ""}.geojson`, JSON.stringify({
type: "FeatureCollection", features: maproulette,
}, null, " "), "utf-8")
console.log("Skipped", skipped, "benches as already linked/imported")
writeFileSync(
`openbenches_export_maproulette${createTest ? "_test" : ""}.geojson`,
JSON.stringify(
{
type: "FeatureCollection",
features: maproulette,
},
null,
" "
),
"utf-8"
)
await this.conflate(osmData.features, { type: "FeatureCollection", features }, "_all")
}
}