Merge branch 'develop'
This commit is contained in:
commit
bb1ef10e23
18 changed files with 1062 additions and 212 deletions
|
@ -4,11 +4,11 @@
|
|||
tasks which can be solved in a few minutes.
|
||||
|
||||
A perfect example of this is to setup such a challenge to e.g. import new points. [Important: always follow the import guidelines if you want to import data.](https://wiki.openstreetmap.org/wiki/Import/Guidelines)
|
||||
(Another approach to set up a guided import is to create a map note for every point with the [import helper](https://mapcomplete.osm.be/import_helper). This however litters the map and will upset mappers if used with to much points.)
|
||||
(Another approach to set up a guided import is to create a map note for every point with the [import helper](https://mapcomplete.osm.be/import_helper). This however litters the map notes and will upset mappers if used with to much points. However, this flow is easier to setup as no changes to theme files are needed, nor is a maproulette-account needed)
|
||||
|
||||
## The API
|
||||
|
||||
**Most of the heavy lifting is done in layer `maproulette`. 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.
|
||||
|
||||
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
|
||||
|
@ -90,21 +90,28 @@ The following example uses the calculated tags `_has_closeby_feature` and `_clos
|
|||
"message": {
|
||||
"en": "Add all the suggested tags"
|
||||
},
|
||||
"image": "./assets/svg/addSmall.svg",
|
||||
"image": "./assets/svg/addSmall.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Changing the status of the task
|
||||
|
||||
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).
|
||||
|
||||
To create an import dataset, make a geojson file where every feature has a `tags`-field with ';'-seperated tags to add.
|
||||
Furthermore, setting the property `blurb` can be useful.
|
||||
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.)
|
||||
|
@ -117,8 +124,8 @@ Furthermore, setting the property `blurb` can be useful.
|
|||
"type": "Feature",
|
||||
"geometry": {"type": "Point", "coordinates": [1.234, 5.678]},
|
||||
"properties": {
|
||||
"id": ...
|
||||
"tags": "foo=bar;name=xyz",
|
||||
"blurb": "Please review this item and add it..."
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ This tagrendering has no question and is thus read-only
|
|||
|
||||
|
||||
|
||||
### blurb
|
||||
### mark_fixed
|
||||
|
||||
|
||||
|
||||
|
@ -84,7 +84,15 @@ This tagrendering has no question and is thus read-only
|
|||
|
||||
|
||||
|
||||
This tagrendering is only visible in the popup if the following condition is met: `blurb~.+`
|
||||
|
||||
|
||||
### mark_duplicate
|
||||
|
||||
|
||||
|
||||
This tagrendering has no question and is thus read-only
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -99,18 +99,6 @@ This tagrendering has no question and is thus read-only
|
|||
|
||||
|
||||
|
||||
### blurb
|
||||
|
||||
|
||||
|
||||
This tagrendering has no question and is thus read-only
|
||||
|
||||
|
||||
|
||||
This tagrendering is only visible in the popup if the following condition is met: `blurb~.+`
|
||||
|
||||
|
||||
|
||||
#### Filters
|
||||
|
||||
|
||||
|
|
|
@ -118,6 +118,8 @@ In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "ar
|
|||
* [Example usage of title](#example-usage-of-title)
|
||||
+ [maproulette_task](#maproulette_task)
|
||||
* [Example usage of maproulette_task](#example-usage-of-maproulette_task)
|
||||
+ [maproulette_set_status](#maproulette_set_status)
|
||||
* [Example usage of maproulette_set_status](#example-usage-of-maproulette_set_status)
|
||||
+ [statistics](#statistics)
|
||||
* [Example usage of statistics](#example-usage-of-statistics)
|
||||
+ [send_email](#send_email)
|
||||
|
@ -785,7 +787,23 @@ Id-key | id | The property name where the ID of the note to close can be found
|
|||
|
||||
#### Example usage of add_image_to_note
|
||||
|
||||
`{add_image_to_note(id)}`
|
||||
The following example sets the status to '2' (false positive)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "mark_duplicate",
|
||||
"render": {
|
||||
"special": {
|
||||
"type": "maproulette_set_status",
|
||||
"message": {
|
||||
"en": "Mark as not found or false positive"
|
||||
},
|
||||
"status": "2",
|
||||
"image": "close"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
@ -811,6 +829,25 @@ This reads the property `mr_challengeId` to detect the parent campaign.
|
|||
|
||||
|
||||
|
||||
### maproulette_set_status
|
||||
|
||||
Change the status of the given MapRoulette task
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
message | _undefined_ | A message to show to the user
|
||||
image | confirm | Image to show
|
||||
message_confirm | _undefined_ | What to show when the task is closed, either by the user or was already closed.
|
||||
status | 1 | A statuscode to apply when the button is clicked. 1 = `close`, 2 = `false_positive`, 3 = `skip`, 4 = `deleted`, 5 = `already fixed` (on the map, e.g. for duplicates), 6 = `too hard`
|
||||
maproulette_id | mr_taskId | The property name containing the maproulette id
|
||||
|
||||
|
||||
#### Example usage of maproulette_set_status
|
||||
|
||||
`{maproulette_set_status(,confirm,,1,mr_taskId)}`
|
||||
|
||||
|
||||
|
||||
### statistics
|
||||
|
||||
Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer
|
||||
|
|
|
@ -15,6 +15,11 @@ import togpx from "togpx"
|
|||
import Constants from "../Models/Constants"
|
||||
|
||||
export class GeoOperations {
|
||||
/**
|
||||
* Create a union between two features
|
||||
*/
|
||||
static union = turf.union
|
||||
static intersect = turf.intersect
|
||||
private static readonly _earthRadius = 6378137
|
||||
private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2
|
||||
|
||||
|
@ -158,35 +163,6 @@ export class GeoOperations {
|
|||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function which does the heavy lifting for 'inside'
|
||||
*/
|
||||
private static pointInPolygonCoordinates(
|
||||
x: number,
|
||||
y: number,
|
||||
coordinates: [number, number][][]
|
||||
) {
|
||||
const inside = GeoOperations.pointWithinRing(
|
||||
x,
|
||||
y,
|
||||
/*This is the outer ring of the polygon */ coordinates[0]
|
||||
)
|
||||
if (!inside) {
|
||||
return false
|
||||
}
|
||||
for (let i = 1; i < coordinates.length; i++) {
|
||||
const inHole = GeoOperations.pointWithinRing(
|
||||
x,
|
||||
y,
|
||||
coordinates[i] /* These are inner rings, aka holes*/
|
||||
)
|
||||
if (inHole) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect wether or not the given point is located in the feature
|
||||
*
|
||||
|
@ -620,6 +596,113 @@ export class GeoOperations {
|
|||
return copy
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes two points and finds the geographic bearing between them, i.e. the angle measured in degrees from the north line (0 degrees)
|
||||
*/
|
||||
public static bearing(a: Coord, b: Coord): number {
|
||||
return turf.bearing(a, b)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 'true' if one feature contains the other feature
|
||||
*
|
||||
* const pond: Feature<Polygon, any> = {
|
||||
* "type": "Feature",
|
||||
* "properties": {"natural":"water","water":"pond"},
|
||||
* "geometry": {
|
||||
* "type": "Polygon",
|
||||
* "coordinates": [[
|
||||
* [4.362924098968506,50.8435422298544 ],
|
||||
* [4.363272786140442,50.8435219059949 ],
|
||||
* [4.363213777542114,50.8437420806679 ],
|
||||
* [4.362924098968506,50.8435422298544 ]
|
||||
* ]]}}
|
||||
* const park: Feature<Polygon, any> = {
|
||||
* "type": "Feature",
|
||||
* "properties": {"leisure":"park"},
|
||||
* "geometry": {
|
||||
* "type": "Polygon",
|
||||
* "coordinates": [[
|
||||
* [ 4.36073541641235,50.84323737103244 ],
|
||||
* [ 4.36469435691833, 50.8423905305197 ],
|
||||
* [ 4.36659336090087, 50.8458997374786 ],
|
||||
* [ 4.36254858970642, 50.8468007074916 ],
|
||||
* [ 4.36073541641235, 50.8432373710324 ]
|
||||
* ]]}}
|
||||
* GeoOperations.completelyWithin(pond, park) // => true
|
||||
* GeoOperations.completelyWithin(park, pond) // => false
|
||||
*/
|
||||
static completelyWithin(
|
||||
feature: Feature<Geometry, any>,
|
||||
possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any>
|
||||
): boolean {
|
||||
return booleanWithin(feature, possiblyEncloingFeature)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an intersection between two features.
|
||||
* A new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary
|
||||
*/
|
||||
public static clipWith(toSplit: Feature, boundary: Feature<Polygon>): Feature[] {
|
||||
if (toSplit.geometry.type === "Point") {
|
||||
const p = <Feature<Point>>toSplit
|
||||
if (GeoOperations.inside(p.geometry.coordinates, boundary)) {
|
||||
return [p]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
if (toSplit.geometry.type === "LineString") {
|
||||
const splitup = turf.lineSplit(<Feature<LineString>>toSplit, boundary)
|
||||
const kept = []
|
||||
for (const f of splitup.features) {
|
||||
const ls = <Feature<LineString>>f
|
||||
if (!GeoOperations.inside(GeoOperations.centerpointCoordinates(f), boundary)) {
|
||||
continue
|
||||
}
|
||||
f.properties = { ...toSplit.properties }
|
||||
kept.push(f)
|
||||
}
|
||||
return kept
|
||||
}
|
||||
if (toSplit.geometry.type === "Polygon" || toSplit.geometry.type == "MultiPolygon") {
|
||||
const splitup = turf.intersect(<Feature<Polygon>>toSplit, boundary)
|
||||
splitup.properties = { ...toSplit.properties }
|
||||
return [splitup]
|
||||
}
|
||||
throw "Invalid geometry type with GeoOperations.clipWith: " + toSplit.geometry.type
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function which does the heavy lifting for 'inside'
|
||||
*/
|
||||
private static pointInPolygonCoordinates(
|
||||
x: number,
|
||||
y: number,
|
||||
coordinates: [number, number][][]
|
||||
) {
|
||||
const inside = GeoOperations.pointWithinRing(
|
||||
x,
|
||||
y,
|
||||
/*This is the outer ring of the polygon */ coordinates[0]
|
||||
)
|
||||
if (!inside) {
|
||||
return false
|
||||
}
|
||||
for (let i = 1; i < coordinates.length; i++) {
|
||||
const inHole = GeoOperations.pointWithinRing(
|
||||
x,
|
||||
y,
|
||||
coordinates[i] /* These are inner rings, aka holes*/
|
||||
)
|
||||
if (inHole) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private static pointWithinRing(x: number, y: number, ring: [number, number][]) {
|
||||
let inside = false
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
||||
|
@ -740,57 +823,4 @@ export class GeoOperations {
|
|||
}
|
||||
throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes two points and finds the geographic bearing between them, i.e. the angle measured in degrees from the north line (0 degrees)
|
||||
*/
|
||||
public static bearing(a: Coord, b: Coord): number {
|
||||
return turf.bearing(a, b)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 'true' if one feature contains the other feature
|
||||
*
|
||||
* const pond: Feature<Polygon, any> = {
|
||||
* "type": "Feature",
|
||||
* "properties": {"natural":"water","water":"pond"},
|
||||
* "geometry": {
|
||||
* "type": "Polygon",
|
||||
* "coordinates": [[
|
||||
* [4.362924098968506,50.8435422298544 ],
|
||||
* [4.363272786140442,50.8435219059949 ],
|
||||
* [4.363213777542114,50.8437420806679 ],
|
||||
* [4.362924098968506,50.8435422298544 ]
|
||||
* ]]}}
|
||||
* const park: Feature<Polygon, any> = {
|
||||
* "type": "Feature",
|
||||
* "properties": {"leisure":"park"},
|
||||
* "geometry": {
|
||||
* "type": "Polygon",
|
||||
* "coordinates": [[
|
||||
* [ 4.36073541641235,50.84323737103244 ],
|
||||
* [ 4.36469435691833, 50.8423905305197 ],
|
||||
* [ 4.36659336090087, 50.8458997374786 ],
|
||||
* [ 4.36254858970642, 50.8468007074916 ],
|
||||
* [ 4.36073541641235, 50.8432373710324 ]
|
||||
* ]]}}
|
||||
* GeoOperations.completelyWithin(pond, park) // => true
|
||||
* GeoOperations.completelyWithin(park, pond) // => false
|
||||
*/
|
||||
static completelyWithin(
|
||||
feature: Feature<Geometry, any>,
|
||||
possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any>
|
||||
): boolean {
|
||||
return booleanWithin(feature, possiblyEncloingFeature)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a union between two features
|
||||
*/
|
||||
static union = turf.union
|
||||
|
||||
/**
|
||||
* Create an intersection between two features
|
||||
*/
|
||||
static intersect = turf.intersect
|
||||
}
|
||||
|
|
|
@ -1,7 +1,27 @@
|
|||
import Constants from "../Models/Constants"
|
||||
|
||||
export default class Maproulette {
|
||||
/**
|
||||
public static readonly STATUS_OPEN = 0
|
||||
public static readonly STATUS_FIXED = 1
|
||||
public static readonly STATUS_FALSE_POSITIVE = 2
|
||||
public static readonly STATUS_SKIPPED = 3
|
||||
public static readonly STATUS_DELETED = 4
|
||||
public static readonly STATUS_ALREADY_FIXED = 5
|
||||
public static readonly STATUS_TOO_HARD = 6
|
||||
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",
|
||||
}
|
||||
|
||||
/*
|
||||
* The API endpoint to use
|
||||
*/
|
||||
endpoint: string
|
||||
|
@ -21,19 +41,34 @@ export default class Maproulette {
|
|||
}
|
||||
|
||||
/**
|
||||
* Close a task
|
||||
* Close a task; might throw an error
|
||||
*
|
||||
* Also see:https://maproulette.org/docs/swagger-ui/index.html?url=/assets/swagger.json&docExpansion=none#/Task/setTaskStatus
|
||||
* @param taskId The task to close
|
||||
* @param status A number indicating the status. Use MapRoulette.STATUS_*
|
||||
* @param options Additional settings to pass. Refer to the API-docs for more information
|
||||
*/
|
||||
async closeTask(taskId: number): Promise<void> {
|
||||
const response = await fetch(`${this.endpoint}/task/${taskId}/1`, {
|
||||
async closeTask(
|
||||
taskId: number,
|
||||
status = Maproulette.STATUS_FIXED,
|
||||
options?: {
|
||||
comment?: string
|
||||
tags?: string
|
||||
requestReview?: boolean
|
||||
completionResponses?: Record<string, string>
|
||||
}
|
||||
): Promise<void> {
|
||||
const response = await fetch(`${this.endpoint}/task/${taskId}/${status}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
apiKey: this.apiKey,
|
||||
},
|
||||
body: options !== undefined ? JSON.stringify(options) : undefined,
|
||||
})
|
||||
if (response.status !== 304) {
|
||||
if (response.status !== 204) {
|
||||
console.log(`Failed to close task: ${response.status}`)
|
||||
throw `Failed to close task: ${response.status}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -227,7 +227,7 @@ export class DownloadPanel extends Toggle {
|
|||
bbox,
|
||||
new Set(neededLayers)
|
||||
)
|
||||
outer: for (const tile of featureList) {
|
||||
for (const tile of featureList) {
|
||||
if (Constants.priviliged_layers.indexOf(tile.layer) >= 0) {
|
||||
continue
|
||||
}
|
||||
|
@ -238,7 +238,7 @@ export class DownloadPanel extends Toggle {
|
|||
}
|
||||
const featureList = perLayer.get(tile.layer)
|
||||
const filters = layer.appliedFilters.data
|
||||
for (const feature of tile.features) {
|
||||
perfeature: for (const feature of tile.features) {
|
||||
if (!bbox.overlapsWith(BBox.get(feature))) {
|
||||
continue
|
||||
}
|
||||
|
@ -250,7 +250,7 @@ export class DownloadPanel extends Toggle {
|
|||
continue
|
||||
}
|
||||
if (!filter.currentFilter.matchesProperties(feature.properties)) {
|
||||
continue outer
|
||||
continue perfeature
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -281,7 +281,7 @@ export class DownloadPanel extends Toggle {
|
|||
delete feature.properties[key]
|
||||
}
|
||||
|
||||
featureList.push(feature)
|
||||
featureList.push(cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -702,7 +702,7 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
Hash.hash.setData(newElementAction.newElementId)
|
||||
|
||||
if (note_id !== undefined) {
|
||||
state.osmConnection.closeNote(note_id, "imported")
|
||||
await state.osmConnection.closeNote(note_id, "imported")
|
||||
originalFeatureTags.data["closed_at"] = new Date().toISOString()
|
||||
originalFeatureTags.ping()
|
||||
}
|
||||
|
@ -720,7 +720,7 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
)
|
||||
} else {
|
||||
console.log("Marking maproulette task as fixed")
|
||||
state.maprouletteConnection.closeTask(Number(maproulette_id))
|
||||
await state.maprouletteConnection.closeTask(Number(maproulette_id))
|
||||
originalFeatureTags.data["mr_taskStatus"] = "Fixed"
|
||||
originalFeatureTags.ping()
|
||||
}
|
||||
|
|
|
@ -52,10 +52,90 @@ import StatisticsPanel from "./BigComponents/StatisticsPanel"
|
|||
import AutoApplyButton from "./Popup/AutoApplyButton"
|
||||
import { LanguageElement } from "./Popup/LanguageElement"
|
||||
import FeatureReviews from "../Logic/Web/MangroveReviews"
|
||||
import Maproulette from "../Logic/Maproulette"
|
||||
|
||||
export default class SpecialVisualizations {
|
||||
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
||||
|
||||
public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined {
|
||||
if (typeof viz === "string") {
|
||||
viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
|
||||
}
|
||||
if (viz === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return new Combine([
|
||||
new Title(viz.funcName, 3),
|
||||
viz.docs,
|
||||
viz.args.length > 0
|
||||
? new Table(
|
||||
["name", "default", "description"],
|
||||
viz.args.map((arg) => {
|
||||
let defaultArg = arg.defaultValue ?? "_undefined_"
|
||||
if (defaultArg == "") {
|
||||
defaultArg = "_empty string_"
|
||||
}
|
||||
return [arg.name, defaultArg, arg.doc]
|
||||
})
|
||||
)
|
||||
: undefined,
|
||||
new Title("Example usage of " + viz.funcName, 4),
|
||||
new FixedUiElement(
|
||||
viz.example ??
|
||||
"`{" +
|
||||
viz.funcName +
|
||||
"(" +
|
||||
viz.args.map((arg) => arg.defaultValue).join(",") +
|
||||
")}`"
|
||||
).SetClass("literal-code"),
|
||||
])
|
||||
}
|
||||
|
||||
public static HelpMessage() {
|
||||
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
|
||||
SpecialVisualizations.DocumentationFor(viz)
|
||||
)
|
||||
|
||||
return new Combine([
|
||||
new Combine([
|
||||
new Title("Special tag renderings", 1),
|
||||
|
||||
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
|
||||
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
|
||||
new Title("Using expanded syntax", 4),
|
||||
`Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`,
|
||||
new FixedUiElement(
|
||||
JSON.stringify(
|
||||
{
|
||||
render: {
|
||||
special: {
|
||||
type: "some_special_visualisation",
|
||||
argname: "some_arg",
|
||||
message: {
|
||||
en: "some other really long message",
|
||||
nl: "een boodschap in een andere taal",
|
||||
},
|
||||
other_arg_name: "more args",
|
||||
},
|
||||
before: {
|
||||
en: "Some text to prefix before the special element (e.g. a title)",
|
||||
nl: "Een tekst om voor het element te zetten (bv. een titel)",
|
||||
},
|
||||
after: {
|
||||
en: "Some text to put after the element, e.g. a footer",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
" "
|
||||
)
|
||||
).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)',
|
||||
]).SetClass("flex flex-col"),
|
||||
...helpTexts,
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
private static initList(): SpecialVisualization[] {
|
||||
const specialVisualizations: SpecialVisualization[] = [
|
||||
new HistogramViz(),
|
||||
|
@ -417,6 +497,24 @@ export default class SpecialVisualizations {
|
|||
defaultValue: "id",
|
||||
},
|
||||
],
|
||||
example:
|
||||
" The following example sets the status to '2' (false positive)\n" +
|
||||
"\n" +
|
||||
"```json\n" +
|
||||
"{\n" +
|
||||
' "id": "mark_duplicate",\n' +
|
||||
' "render": {\n' +
|
||||
' "special": {\n' +
|
||||
' "type": "maproulette_set_status",\n' +
|
||||
' "message": {\n' +
|
||||
' "en": "Mark as not found or false positive"\n' +
|
||||
" },\n" +
|
||||
' "status": "2",\n' +
|
||||
' "image": "close"\n' +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"}\n" +
|
||||
"```",
|
||||
constr: (state, tags, args) => {
|
||||
const isUploading = new UIEventSource(false)
|
||||
const t = Translations.t.notes
|
||||
|
@ -517,6 +615,101 @@ export default class SpecialVisualizations {
|
|||
},
|
||||
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.",
|
||||
},
|
||||
{
|
||||
funcName: "maproulette_set_status",
|
||||
docs: "Change the status of the given MapRoulette task",
|
||||
args: [
|
||||
{
|
||||
name: "message",
|
||||
doc: "A message to show to the user",
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
doc: "Image to show",
|
||||
defaultValue: "confirm",
|
||||
},
|
||||
{
|
||||
name: "message_confirm",
|
||||
doc: "What to show when the task is closed, either by the user or was already closed.",
|
||||
},
|
||||
{
|
||||
name: "status",
|
||||
doc: "A statuscode to apply when the button is clicked. 1 = `close`, 2 = `false_positive`, 3 = `skip`, 4 = `deleted`, 5 = `already fixed` (on the map, e.g. for duplicates), 6 = `too hard`",
|
||||
defaultValue: "1",
|
||||
},
|
||||
{
|
||||
name: "maproulette_id",
|
||||
doc: "The property name containing the maproulette id",
|
||||
defaultValue: "mr_taskId",
|
||||
},
|
||||
],
|
||||
constr: (state, tagsSource, args, guistate) => {
|
||||
let [message, image, message_closed, status, maproulette_id_key] = args
|
||||
if (image === "") {
|
||||
image = "confirm"
|
||||
}
|
||||
if (Svg.All[image] !== undefined || Svg.All[image + ".svg"] !== undefined) {
|
||||
if (image.endsWith(".svg")) {
|
||||
image = image.substring(0, image.length - 4)
|
||||
}
|
||||
image = Svg[image + "_ui"]()
|
||||
}
|
||||
const failed = new UIEventSource(false)
|
||||
|
||||
const closeButton = new SubtleButton(image, message).OnClickWithLoading(
|
||||
Translations.t.general.loading,
|
||||
async () => {
|
||||
const maproulette_id =
|
||||
tagsSource.data[maproulette_id_key] ?? tagsSource.data.id
|
||||
try {
|
||||
await state.maprouletteConnection.closeTask(
|
||||
Number(maproulette_id),
|
||||
Number(status),
|
||||
{
|
||||
tags: `MapComplete MapComplete:${state.layoutToUse.id}`,
|
||||
}
|
||||
)
|
||||
tagsSource.data["mr_taskStatus"] =
|
||||
Maproulette.STATUS_MEANING[Number(status)]
|
||||
tagsSource.data.status = status
|
||||
tagsSource.ping()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
failed.setData(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let message_closed_element = undefined
|
||||
if (message_closed !== undefined && message_closed !== "") {
|
||||
message_closed_element = new FixedUiElement(message_closed)
|
||||
}
|
||||
|
||||
return new VariableUiElement(
|
||||
tagsSource
|
||||
.map(
|
||||
(tgs) =>
|
||||
tgs["status"] ??
|
||||
Maproulette.STATUS_MEANING[tgs["mr_taskStatus"]]
|
||||
)
|
||||
.map(Number)
|
||||
.map(
|
||||
(status) => {
|
||||
if (failed.data) {
|
||||
return new FixedUiElement(
|
||||
"ERROR - could not close the MapRoulette task"
|
||||
).SetClass("block alert")
|
||||
}
|
||||
if (status === Maproulette.STATUS_OPEN) {
|
||||
return closeButton
|
||||
}
|
||||
return message_closed_element ?? "Closed!"
|
||||
},
|
||||
[failed]
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "statistics",
|
||||
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
|
||||
|
@ -674,83 +867,4 @@ export default class SpecialVisualizations {
|
|||
|
||||
return specialVisualizations
|
||||
}
|
||||
|
||||
public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined {
|
||||
if (typeof viz === "string") {
|
||||
viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
|
||||
}
|
||||
if (viz === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return new Combine([
|
||||
new Title(viz.funcName, 3),
|
||||
viz.docs,
|
||||
viz.args.length > 0
|
||||
? new Table(
|
||||
["name", "default", "description"],
|
||||
viz.args.map((arg) => {
|
||||
let defaultArg = arg.defaultValue ?? "_undefined_"
|
||||
if (defaultArg == "") {
|
||||
defaultArg = "_empty string_"
|
||||
}
|
||||
return [arg.name, defaultArg, arg.doc]
|
||||
})
|
||||
)
|
||||
: undefined,
|
||||
new Title("Example usage of " + viz.funcName, 4),
|
||||
new FixedUiElement(
|
||||
viz.example ??
|
||||
"`{" +
|
||||
viz.funcName +
|
||||
"(" +
|
||||
viz.args.map((arg) => arg.defaultValue).join(",") +
|
||||
")}`"
|
||||
).SetClass("literal-code"),
|
||||
])
|
||||
}
|
||||
|
||||
public static HelpMessage() {
|
||||
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
|
||||
SpecialVisualizations.DocumentationFor(viz)
|
||||
)
|
||||
|
||||
return new Combine([
|
||||
new Combine([
|
||||
new Title("Special tag renderings", 1),
|
||||
|
||||
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
|
||||
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
|
||||
new Title("Using expanded syntax", 4),
|
||||
`Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`,
|
||||
new FixedUiElement(
|
||||
JSON.stringify(
|
||||
{
|
||||
render: {
|
||||
special: {
|
||||
type: "some_special_visualisation",
|
||||
argname: "some_arg",
|
||||
message: {
|
||||
en: "some other really long message",
|
||||
nl: "een boodschap in een andere taal",
|
||||
},
|
||||
other_arg_name: "more args",
|
||||
},
|
||||
before: {
|
||||
en: "Some text to prefix before the special element (e.g. a title)",
|
||||
nl: "Een tekst om voor het element te zetten (bv. een titel)",
|
||||
},
|
||||
after: {
|
||||
en: "Some text to put after the element, e.g. a footer",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
" "
|
||||
)
|
||||
).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)',
|
||||
]).SetClass("flex flex-col"),
|
||||
...helpTexts,
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
}
|
||||
|
|
365
assets/layers/clock/clock.json
Normal file
365
assets/layers/clock/clock.json
Normal file
|
@ -0,0 +1,365 @@
|
|||
{
|
||||
"id": "clock",
|
||||
"name": {
|
||||
"en": "Clocks",
|
||||
"nl": "Klokken"
|
||||
},
|
||||
"description": {
|
||||
"en": "Layer with public clocks",
|
||||
"nl": "Laag met publieke klokken"
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Clock",
|
||||
"nl": "Klok"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"osmTags": "amenity=clock"
|
||||
},
|
||||
"minzoom": 13,
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"id": "support",
|
||||
"question": {
|
||||
"en": "In what way is the clock mounted?",
|
||||
"nl": "Hoe is de klok bevestigd?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "support=pole",
|
||||
"then": {
|
||||
"en": "This clock is mounted on a pole",
|
||||
"nl": "Deze klok is bevestigd aan een paal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "support=wall_mounted",
|
||||
"then": {
|
||||
"en": "This clock is mounted on a wall",
|
||||
"nl": "Deze klok is bevestigd aan een muur"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "support=billboard",
|
||||
"then": {
|
||||
"en": "This clock is part of a billboard",
|
||||
"nl": "Deze klok is onderdeel van een reclamebord"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "support=ground",
|
||||
"then": {
|
||||
"en": "This clock is on the ground",
|
||||
"nl": "Deze klok staat op de grond"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "display",
|
||||
"question": {
|
||||
"en": "How does this clock display the time?",
|
||||
"nl": "Hoe toont deze klok de tijd?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "display=analog",
|
||||
"then": {
|
||||
"en": "This clock displays the time with hands",
|
||||
"nl": "Deze klok toont de tijd met wijzers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "display=digital",
|
||||
"then": {
|
||||
"en": "This clock displays the time with digits",
|
||||
"nl": "Deze klok toont de tijd met cijfers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "display=sundial",
|
||||
"then": {
|
||||
"en": "This clock displays the time with a sundial",
|
||||
"nl": "Deze klok toont de tijd met een zonnewijzer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "display=unorthodox",
|
||||
"then": {
|
||||
"en": "This clock displays the time in a non-standard way, e.g using binary, water or something else",
|
||||
"nl": "Deze klok toont de tijd op een niet-standaard manier, bijvoorbeeld met binaire cijfers, water of iets anders"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "visibility",
|
||||
"question": {
|
||||
"en": "How visible is this clock?",
|
||||
"nl": "Hoe zichtbaar is deze klok?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "visibility=house",
|
||||
"then": {
|
||||
"en": "This clock is visible from about 5 meters away (small wall-mounted clock)",
|
||||
"nl": "Deze klok is zichtbaar vanaf ongeveer 5 meter afstand (kleine klok aan een muur)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "visibility=street",
|
||||
"then": {
|
||||
"en": "This clock is visible from about 20 meters away (medium size billboard clock)",
|
||||
"nl": "Deze klok is zichtbaar vanaf ongeveer 20 meter afstand (klok op gemiddeld reclamebord)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "visibility=area",
|
||||
"then": {
|
||||
"en": "This clock is visible from more than 20 meters away (church clock)",
|
||||
"nl": "Deze klok is zichtbaar vanaf meer dan 20 meter afstand (kerkklok)"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "date",
|
||||
"question": {
|
||||
"en": "Does this clock also display the date?",
|
||||
"nl": "Toont deze klok ook de datum?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "date=yes",
|
||||
"then": {
|
||||
"en": "This clock also displays the date",
|
||||
"nl": "Deze klok toont ook de datum"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "date=no",
|
||||
"then": {
|
||||
"en": "This clock does not display the date",
|
||||
"nl": "Deze klok toont de datum niet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "date=",
|
||||
"then": {
|
||||
"en": "This clock does probably not display the date",
|
||||
"nl": "Deze klok toont de datum waarschijnlijk niet"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "thermometer",
|
||||
"question": {
|
||||
"en": "Does this clock also display the temperature?",
|
||||
"nl": "Toont deze klok ook de temperatuur?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "thermometer=yes",
|
||||
"then": {
|
||||
"en": "This clock also displays the temperature",
|
||||
"nl": "Deze klok toont ook de temperatuur"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "thermometer=no",
|
||||
"then": {
|
||||
"en": "This clock does not display the temperature",
|
||||
"nl": "Deze klok toont de temperatuur niet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "thermometer=",
|
||||
"then": {
|
||||
"en": "This clock does probably not display the temperature",
|
||||
"nl": "Deze klok toont de temperatuur waarschijnlijk niet"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "barometer",
|
||||
"question": {
|
||||
"en": "Does this clock also display the air pressure?",
|
||||
"nl": "Toont deze klok ook de luchtdruk?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "barometer=yes",
|
||||
"then": {
|
||||
"en": "This clock also displays the air pressure",
|
||||
"nl": "Deze klok toont ook de luchtdruk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "barometer=no",
|
||||
"then": {
|
||||
"en": "This clock does not display the air pressure",
|
||||
"nl": "Deze klok toont de luchtdruk niet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "barometer=",
|
||||
"then": {
|
||||
"en": "This clock does probably not display the air pressure",
|
||||
"nl": "Deze klok toont de luchtdruk waarschijnlijk niet"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "hygrometer",
|
||||
"question": {
|
||||
"en": "Does this clock also display the humidity?",
|
||||
"nl": "Toont deze klok ook de luchtvochtigheid?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "hygrometer=yes",
|
||||
"then": {
|
||||
"en": "This clock also displays the humidity",
|
||||
"nl": "Deze klok toont ook de luchtvochtigheid"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "hygrometer=no",
|
||||
"then": {
|
||||
"en": "This clock does not display the humidity",
|
||||
"nl": "Deze klok toont de luchtvochtigheid niet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "hygrometer=",
|
||||
"then": {
|
||||
"en": "This clock does probably not display the humidity",
|
||||
"nl": "Deze klok toont de luchtvochtigheid waarschijnlijk niet"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "faces",
|
||||
"question": {
|
||||
"en": "How many faces does this clock have?",
|
||||
"nl": "Hoeveel klokken heeft deze klok?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "faces",
|
||||
"type": "pnat",
|
||||
"inline": true,
|
||||
"placeholder": {
|
||||
"en": "Number of faces",
|
||||
"nl": "Aantal klokken"
|
||||
}
|
||||
},
|
||||
"render": {
|
||||
"en": "This clock has {faces} faces",
|
||||
"nl": "Deze klok heeft {faces} klokken"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "faces=1",
|
||||
"then": {
|
||||
"en": "This clock has one face",
|
||||
"nl": "Deze klok heeft één klok"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "faces=2",
|
||||
"then": {
|
||||
"en": "This clock has two faces",
|
||||
"nl": "Deze klok heeft twee klokken"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "faces=4",
|
||||
"then": {
|
||||
"en": "This clock has four faces",
|
||||
"nl": "Deze klok heeft vier klokken"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"presets": [
|
||||
{
|
||||
"tags": [
|
||||
"amenity=clock"
|
||||
],
|
||||
"title": {
|
||||
"en": "a clock",
|
||||
"nl": "een klok"
|
||||
},
|
||||
"description": {
|
||||
"en": "A publicly visible clock",
|
||||
"nl": "Een publiekelijk zichtbare klok"
|
||||
},
|
||||
"preciseInput": {
|
||||
"preferredBackground": [
|
||||
"photo",
|
||||
"map"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tags": [
|
||||
"amenity=clock",
|
||||
"support=wall_mounted"
|
||||
],
|
||||
"title": {
|
||||
"en": "a wall-mounted clock",
|
||||
"nl": "een klok aan een muur"
|
||||
},
|
||||
"description": {
|
||||
"en": "A publicly visible clock mounted on a wall",
|
||||
"nl": "Een publiekelijk zichtbare klok aan een muur"
|
||||
},
|
||||
"preciseInput": {
|
||||
"preferredBackground": [
|
||||
"photo",
|
||||
"map"
|
||||
],
|
||||
"snapToLayer": "walls_and_buildings"
|
||||
}
|
||||
}
|
||||
],
|
||||
"allowMove": true,
|
||||
"deletion": true,
|
||||
"mapRendering": [
|
||||
{
|
||||
"location": [
|
||||
"point",
|
||||
"centroid"
|
||||
],
|
||||
"icon": {
|
||||
"render": "./assets/layers/clock/clock.svg",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "display=digital",
|
||||
"then": "./assets/layers/clock/clock_digital.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
"iconSize": {
|
||||
"render": "40,40,center",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "display=digital",
|
||||
"then": "50,20,center"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
138
assets/layers/clock/clock.svg
Normal file
138
assets/layers/clock/clock.svg
Normal file
|
@ -0,0 +1,138 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Ebene_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="500"
|
||||
height="500"
|
||||
viewBox="0 0 500 500"
|
||||
enable-background="new 0 0 20 20"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="clock.svg"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs14" /><sodipodi:namedview
|
||||
id="namedview12"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.36875"
|
||||
inkscape:cx="364.74576"
|
||||
inkscape:cy="35.254237"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g9" />
|
||||
<g
|
||||
id="g9">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<g
|
||||
id="g922"
|
||||
transform="matrix(25.000002,0,0,25.000002,-2.0027162e-5,-2.0027162e-5)"><circle
|
||||
id="path2931-3"
|
||||
inkscape:export-filename="/home/markus/Downloads/1.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
fill="#f2f2f2"
|
||||
stroke="#734a08"
|
||||
cx="10"
|
||||
cy="10"
|
||||
style="stroke-width:1.17647"
|
||||
r="9.4117641" /><rect
|
||||
id="rect2935"
|
||||
x="2.674"
|
||||
y="9.5"
|
||||
inkscape:export-filename="/home/markus/Downloads/1.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
opacity="0.5"
|
||||
fill="#4d4d4d"
|
||||
enable-background="new "
|
||||
width="2.326"
|
||||
height="1.108">
|
||||
</rect><rect
|
||||
id="rect2935-7"
|
||||
x="15.176"
|
||||
y="9.5"
|
||||
inkscape:export-filename="/home/markus/Downloads/1.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
opacity="0.5"
|
||||
fill="#4d4d4d"
|
||||
enable-background="new "
|
||||
width="2.3239999"
|
||||
height="1.108">
|
||||
</rect><rect
|
||||
id="rect2935-7-1"
|
||||
x="9.5"
|
||||
y="2.6760001"
|
||||
inkscape:export-filename="/home/markus/Downloads/1.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
opacity="0.5"
|
||||
fill="#4d4d4d"
|
||||
enable-background="new "
|
||||
width="1.108"
|
||||
height="2.3239999">
|
||||
</rect><rect
|
||||
id="rect2935-7-1-2"
|
||||
x="9.5"
|
||||
y="15"
|
||||
inkscape:export-filename="/home/markus/Downloads/1.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
opacity="0.5"
|
||||
fill="#4d4d4d"
|
||||
enable-background="new "
|
||||
width="1.108"
|
||||
height="2.326">
|
||||
</rect><rect
|
||||
id="rect2935-7-1-6"
|
||||
x="3.9330001"
|
||||
y="7.0009999"
|
||||
inkscape:export-filename="/home/markus/Downloads/1.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
transform="matrix(-0.5,-0.866,0.866,-0.5,5.5694,18.4072)"
|
||||
fill="#734a08"
|
||||
width="8.3290005"
|
||||
height="1.1900001">
|
||||
</rect><rect
|
||||
id="rect2935-7-1-4"
|
||||
x="8.9049997"
|
||||
y="9.1400003"
|
||||
inkscape:export-filename="/home/markus/Downloads/1.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
transform="matrix(0.9068,-0.4215,0.4215,0.9068,-2.9963,5.9137)"
|
||||
fill="#734a08"
|
||||
width="5.9489999"
|
||||
height="1.189">
|
||||
</rect></g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
5
assets/layers/clock/clock_digital.svg
Normal file
5
assets/layers/clock/clock_digital.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="900" height="358" viewBox="0 0 900 358" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="900" height="358" fill="#D9D9D9"/>
|
||||
<rect x="40" y="40" width="820" height="278" fill="black"/>
|
||||
<path d="M181.2 278.8L159.6 258.2V209L181.2 189.4V278.8ZM180.6 93V182.4L159 163V113.6L180.6 93ZM245.361 287.6L265.961 266H315.361L334.761 287.6H245.361ZM239.361 189.4L260.961 210.2V259.2L239.361 278.8V189.4ZM262.561 196.6C257.894 192.867 253.161 189.2 248.361 185.6L262.561 175H317.161L331.161 185.6L317.161 196.6H262.561ZM245.161 84.4H334.561L314.761 106H265.761L245.161 84.4ZM340.561 93V182.4L318.961 163V113.6L340.561 93ZM437.322 187.4H462.522V212.4H437.322V187.4ZM437.322 128H462.522V153.6H437.322V128ZM565.283 287.6L585.883 266H635.283L654.683 287.6H565.283ZM559.283 189.4L580.883 210.2V259.2L559.283 278.8V189.4ZM661.083 278.8L639.483 258.2V209L661.083 189.4V278.8ZM565.083 84.4H654.483L634.683 106H585.683L565.083 84.4ZM558.683 182.4V93L580.283 112.4V162L558.683 182.4ZM660.483 93V182.4L638.883 163V113.6L660.483 93ZM821.044 278.8L799.444 258.2V209L821.044 189.4V278.8ZM820.444 93V182.4L798.844 163V113.6L820.444 93Z" fill="#FF0000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
20
assets/layers/clock/license_info.json
Normal file
20
assets/layers/clock/license_info.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
[
|
||||
{
|
||||
"path": "clock.svg",
|
||||
"license": "CC0",
|
||||
"authors": [
|
||||
"SJJB Management"
|
||||
],
|
||||
"sources": [
|
||||
"https://wiki.openstreetmap.org/wiki/File:Amenity_clock_20x20.svg"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "clock_digital.svg",
|
||||
"license": "CC0",
|
||||
"authors": [
|
||||
"Robin van der Linde"
|
||||
],
|
||||
"sources": []
|
||||
}
|
||||
]
|
|
@ -53,7 +53,7 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"iconSize": "40,40,center"
|
||||
"iconSize": "40,40,bottom"
|
||||
}
|
||||
],
|
||||
"tagRenderings": [
|
||||
|
@ -128,9 +128,41 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"id": "blurb",
|
||||
"condition": "blurb~*",
|
||||
"render": "{blurb}"
|
||||
"id": "mark_fixed",
|
||||
"render": {
|
||||
"special": {
|
||||
"type": "maproulette_set_status",
|
||||
"message": {
|
||||
"en": "Mark as fixed"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mark_duplicate",
|
||||
"render": {
|
||||
"special": {
|
||||
"type": "maproulette_set_status",
|
||||
"message": {
|
||||
"en": "Mark as not found or false positive"
|
||||
},
|
||||
"status": "2",
|
||||
"image": "close"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mark_too_hard",
|
||||
"render": {
|
||||
"special": {
|
||||
"type": "maproulette_set_status",
|
||||
"message": {
|
||||
"en": "Mark as too hard"
|
||||
},
|
||||
"status": "6",
|
||||
"image": "not_found"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"minzoom": 15,
|
||||
|
@ -266,4 +298,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,11 +144,6 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "blurb",
|
||||
"condition": "blurb~*",
|
||||
"render": "{blurb}"
|
||||
}
|
||||
],
|
||||
"filter": [
|
||||
|
@ -229,4 +224,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
18
assets/themes/clock/clock.json
Normal file
18
assets/themes/clock/clock.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "clock",
|
||||
"title": {
|
||||
"en": "Clocks",
|
||||
"nl": "Klokken"
|
||||
},
|
||||
"description": {
|
||||
"en": "Map showing all public clocks",
|
||||
"nl": "Kaart met alle openbare klokken"
|
||||
},
|
||||
"icon": "./assets/layers/clock/clock.svg",
|
||||
"startLat": 0,
|
||||
"startLon": 0,
|
||||
"startZoom": 0,
|
||||
"layers": [
|
||||
"clock"
|
||||
]
|
||||
}
|
|
@ -24,6 +24,9 @@ import { GeoOperations } from "../Logic/GeoOperations"
|
|||
import SimpleMetaTaggers from "../Logic/SimpleMetaTagger"
|
||||
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
|
||||
import Loc from "../Models/Loc"
|
||||
import { Feature } from "geojson"
|
||||
import { BBox } from "../Logic/BBox"
|
||||
import { bboxClip } from "@turf/turf"
|
||||
|
||||
ScriptUtils.fixUtils()
|
||||
|
||||
|
@ -232,7 +235,8 @@ function sliceToTiles(
|
|||
theme: LayoutConfig,
|
||||
relationsTracker: RelationsTracker,
|
||||
targetdir: string,
|
||||
pointsOnlyLayers: string[]
|
||||
pointsOnlyLayers: string[],
|
||||
clip: boolean
|
||||
) {
|
||||
const skippedLayers = new Set<string>()
|
||||
|
||||
|
@ -310,6 +314,7 @@ function sliceToTiles(
|
|||
maxFeatureCount: undefined,
|
||||
registerTile: (tile) => {
|
||||
const tileIndex = tile.tileIndex
|
||||
const bbox = BBox.fromTileIndex(tileIndex).asGeoJson({})
|
||||
console.log("Got tile:", tileIndex, tile.layer.layerDef.id)
|
||||
if (tile.features.data.length === 0) {
|
||||
return
|
||||
|
@ -343,9 +348,9 @@ function sliceToTiles(
|
|||
}
|
||||
let strictlyCalculated = 0
|
||||
let featureCount = 0
|
||||
for (const feature of filteredTile.features.data) {
|
||||
let features: Feature[] = filteredTile.features.data.map((f) => f.feature)
|
||||
for (const feature of features) {
|
||||
// Some cleanup
|
||||
delete feature.feature["bbox"]
|
||||
|
||||
if (tile.layer.layerDef.calculatedTags !== undefined) {
|
||||
// Evaluate all the calculated tags strictly
|
||||
|
@ -353,7 +358,7 @@ function sliceToTiles(
|
|||
(ct) => ct[0]
|
||||
)
|
||||
featureCount++
|
||||
const props = feature.feature.properties
|
||||
const props = feature.properties
|
||||
for (const calculatedTagKey of calculatedTagKeys) {
|
||||
const strict = props[calculatedTagKey]
|
||||
|
||||
|
@ -379,7 +384,16 @@ function sliceToTiles(
|
|||
}
|
||||
}
|
||||
}
|
||||
delete feature["bbox"]
|
||||
}
|
||||
|
||||
if (clip) {
|
||||
console.log("Clipping features")
|
||||
features = [].concat(
|
||||
...features.map((f: Feature) => GeoOperations.clipWith(<any>f, bbox))
|
||||
)
|
||||
}
|
||||
|
||||
// Lets save this tile!
|
||||
const [z, x, y] = Tiles.tile_from_index(tileIndex)
|
||||
// console.log("Writing tile ", z, x, y, layerId)
|
||||
|
@ -391,7 +405,7 @@ function sliceToTiles(
|
|||
JSON.stringify(
|
||||
{
|
||||
type: "FeatureCollection",
|
||||
features: filteredTile.features.data.map((f) => f.feature),
|
||||
features,
|
||||
},
|
||||
null,
|
||||
" "
|
||||
|
@ -476,8 +490,9 @@ export async function main(args: string[]) {
|
|||
console.log("Cache builder started with args ", args.join(", "))
|
||||
if (args.length < 6) {
|
||||
console.error(
|
||||
"Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...] [--force-zoom-level z] \n" +
|
||||
"Note: a new directory named <theme> will be created in targetdirectory"
|
||||
"Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...] [--force-zoom-level z] [--clip]" +
|
||||
"--force-zoom-level causes non-cached-layers to be donwnloaded\n" +
|
||||
"--clip will erase parts of the feature falling outside of the bounding box"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
@ -494,6 +509,7 @@ export async function main(args: string[]) {
|
|||
const lon0 = Number(args[4])
|
||||
const lat1 = Number(args[5])
|
||||
const lon1 = Number(args[6])
|
||||
const clip = args.indexOf("--clip") >= 0
|
||||
|
||||
if (isNaN(lat0)) {
|
||||
throw "The first number (a latitude) is not a valid number"
|
||||
|
@ -570,7 +586,7 @@ export async function main(args: string[]) {
|
|||
|
||||
const extraFeatures = await downloadExtraData(theme)
|
||||
const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures)
|
||||
sliceToTiles(allFeaturesSource, theme, relationTracker, targetdir, generatePointLayersFor)
|
||||
sliceToTiles(allFeaturesSource, theme, relationTracker, targetdir, generatePointLayersFor, clip)
|
||||
}
|
||||
|
||||
let args = [...process.argv]
|
||||
|
|
|
@ -2,6 +2,7 @@ import { describe } from "mocha"
|
|||
import { expect } from "chai"
|
||||
import * as turf from "@turf/turf"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { Feature, LineString, Polygon } from "geojson"
|
||||
|
||||
describe("GeoOperations", () => {
|
||||
describe("calculateOverlap", () => {
|
||||
|
@ -133,4 +134,45 @@ describe("GeoOperations", () => {
|
|||
expect(overlapsRev).empty
|
||||
})
|
||||
})
|
||||
describe("clipWith", () => {
|
||||
it("clipWith should clip linestrings", () => {
|
||||
const bbox: Feature<Polygon> = {
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: {
|
||||
coordinates: [
|
||||
[
|
||||
[3.218560377159008, 51.21600586532159],
|
||||
[3.218560377159008, 51.21499687768525],
|
||||
[3.2207456783268356, 51.21499687768525],
|
||||
[3.2207456783268356, 51.21600586532159],
|
||||
[3.218560377159008, 51.21600586532159],
|
||||
],
|
||||
],
|
||||
type: "Polygon",
|
||||
},
|
||||
}
|
||||
const line: Feature<LineString> = {
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: {
|
||||
coordinates: [
|
||||
[3.218405371672816, 51.21499091846559],
|
||||
[3.2208408127450525, 51.21560173433727],
|
||||
],
|
||||
type: "LineString",
|
||||
},
|
||||
}
|
||||
const result = GeoOperations.clipWith(line, bbox)
|
||||
expect(result.length).to.equal(1)
|
||||
expect(result[0].geometry.type).to.eq("LineString")
|
||||
const clippedLine = (<Feature<LineString>>result[0]).geometry.coordinates
|
||||
const expCoordinates = [
|
||||
[3.2185604, 51.215029800031594],
|
||||
[3.2207457, 51.21557787977764],
|
||||
]
|
||||
|
||||
expect(clippedLine).to.deep.equal(expCoordinates)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue