Merge develop

This commit is contained in:
pietervdvn 2022-09-29 16:02:21 +02:00
commit f7370d0ae9
25 changed files with 452 additions and 64 deletions

View file

@ -1 +1,2 @@
["file-overview.json","missing_editor.json","stats.2020-10.json","stats.2020-11.json","stats.2020-12.json","stats.2020-5.json","stats.2020-6.json","stats.2020-7.json","stats.2020-8.json","stats.2020-9.json","stats.2021-1.json","stats.2021-10.json","stats.2021-11.json","stats.2021-12.json","stats.2021-2.json","stats.2021-3.json","stats.2021-4.json","stats.2021-5.json","stats.2021-6.json","stats.2021-7.json","stats.2021-8.json","stats.2021-9.json","stats.2022-1.json","stats.2022-2.json","stats.2022-3.json","stats.2022-4.json","stats.2022-5.json","stats.2022-6.json","stats.2022-7.json","stats.2022-8.json","stats.2022-9-01.day.json","stats.2022-9-02.day.json","stats.2022-9-03.day.json","stats.2022-9-04.day.json","stats.2022-9-05.day.json","stats.2022-9-06.day.json","stats.2022-9-07.day.json","stats.2022-9-08.day.json","stats.2022-9-09.day.json","stats.2022-9-10.day.json","stats.2022-9-11.day.json","stats.2022-9-12.day.json","stats.2022-9-13.day.json","stats.2022-9-14.day.json","stats.2022-9-15.day.json","stats.2022-9-16.day.json","stats.2022-9-17.day.json","stats.2022-9-18.day.json","stats.2022-9-19.day.json","stats.2022-9-20.day.json","stats.2022-9-21.day.json","stats.2022-9-22.day.json","stats.2022-9-23.day.json"]
["file-overview.json","missing_editor.json","stats.2020-10.json","stats.2020-11.json","stats.2020-12.json","stats.2020-5.json","stats.2020-6.json","stats.2020-7.json","stats.2020-8.json","stats.2020-9.json","stats.2021-1.json","stats.2021-10.json","stats.2021-11.json","stats.2021-12.json","stats.2021-2.json","stats.2021-3.json","stats.2021-4.json","stats.2021-5.json","stats.2021-6.json","stats.2021-7.json","stats.2021-8.json","stats.2021-9.json","stats.2022-1.json","stats.2022-2.json","stats.2022-3.json","stats.2022-4.json","stats.2022-5.json","stats.2022-6.json","stats.2022-7.json","stats.2022-8.json","stats.2022-9-01.day.json","stats.2022-9-02.day.json","stats.2022-9-03.day.json","stats.2022-9-04.day.json","stats.2022-9-05.day.json","stats.2022-9-06.day.json","stats.2022-9-07.day.json","stats.2022-9-08.day.json","stats.2022-9-09.day.json","stats.2022-9-10.day.json","stats.2022-9-11.day.json","stats.2022-9-12.day.json","stats.2022-9-13.day.json","stats.2022-9-14.day.json","stats.2022-9-15.day.json","stats.2022-9-16.day.json","stats.2022-9-17.day.json","stats.2022-9-18.day.json","stats.2022-9-19.day.json","stats.2022-9-20.day.json","stats.2022-9-21.day.json","stats.2022-9-22.day.json","stats.2022-9-23.day.json","stats.2022-9-24.day.json","stats.2022-9-25.day.json"]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -137,10 +137,13 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
l("Stamen.TonerBackground", "Toner Background - no labels (by Stamen)"),
l("Stamen.Watercolor", "Watercolor (by Stamen)"),
l("Stadia.OSMBright", "Osm Bright (by Stadia)"),
l("Stadia.AlidadeSmoothDark", "Alidade Smooth Dark (by Stadia)"),
l("CartoDB.Positron", "Positron (by CartoDB)"),
l("CartoDB.PositronNoLabels", "Positron - no labels (by CartoDB)"),
l("CartoDB.Voyager", "Voyager (by CartoDB)"),
l("CartoDB.VoyagerNoLabels", "Voyager - no labels (by CartoDB)"),
l("CartoDB.DarkMatter", "Dark Matter (by CartoDB)"),
l("CartoDB.DarkMatterNoLabels", "Dark Matter - no labels (by CartoDB)"),
]
return Utils.NoNull(layers)
}

View file

@ -13,7 +13,7 @@ import {
Polygon,
Properties,
} from "@turf/turf"
import {GeoJSON, LineString} from "geojson";
import {GeoJSON, LineString, Point} from "geojson";
export class GeoOperations {
private static readonly _earthRadius = 6378137
@ -27,8 +27,8 @@ export class GeoOperations {
* Converts a GeoJson feature to a point GeoJson feature
* @param feature
*/
static centerpoint(feature: any) {
const newFeature = turf.center(feature)
static centerpoint(feature: any): Feature<Point> {
const newFeature : Feature<Point> = turf.center(feature)
newFeature.properties = feature.properties
newFeature.id = feature.id
return newFeature

View file

@ -9,6 +9,7 @@ import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import { CountryCoder } from "latlon2country"
import Constants from "../Models/Constants"
import { TagUtils } from "./Tags/TagUtils"
import {Feature, LineString} from "geojson";
export class SimpleMetaTagger {
public readonly keys: string[]
@ -420,6 +421,38 @@ export default class SimpleMetaTaggers {
return true
}
)
private static directionCenterpoint = new SimpleMetaTagger(
{
keys:["_direction:centerpoint"],
isLazy: true,
doc: "_direction:centerpoint is the direction of the linestring (in degrees) if one were standing at the projected centerpoint."
},
(feature: Feature) => {
if(feature.geometry.type !== "LineString"){
return false
}
const ls = <Feature<LineString>> feature;
Object.defineProperty(feature.properties, "_direction:centerpoint", {
enumerable: false,
configurable: true,
get: () => {
const centroid = GeoOperations.centerpoint(feature)
const projected = GeoOperations.nearestPoint(ls, <[number,number]> centroid.geometry.coordinates)
const nextPoint = ls.geometry.coordinates[projected.properties.index + 1]
const bearing = GeoOperations.bearing(projected.geometry.coordinates, nextPoint)
delete feature.properties["_direction:centerpoint"]
feature.properties["_direction:centerpoint"] = bearing
return bearing
},
})
return true
}
)
private static currentTime = new SimpleMetaTagger(
{
keys: ["_now:date", "_now:datetime", "_loaded:date", "_loaded:_datetime"],
@ -457,6 +490,7 @@ export default class SimpleMetaTaggers {
SimpleMetaTaggers.country,
SimpleMetaTaggers.isOpen,
SimpleMetaTaggers.directionSimplified,
SimpleMetaTaggers.directionCenterpoint,
SimpleMetaTaggers.currentTime,
SimpleMetaTaggers.objectMetaInfo,
SimpleMetaTaggers.noBothButLeftRight,

View file

@ -371,7 +371,7 @@ export default class LayerConfig extends WithContextLoader {
throw "Error in " + context + ": use 'filter' instead of 'filters'"
}
this.titleIcons = this.ParseTagRenderings(<TagRenderingConfigJson[]>json.titleIcons, {
this.titleIcons = this.ParseTagRenderings(<TagRenderingConfigJson[]>json.titleIcons ?? [], {
readOnlyMode: true,
})

View file

@ -148,7 +148,7 @@ export default abstract class BaseUIElement {
} catch (e) {
const domExc = e as DOMException
if (domExc) {
console.log("An exception occured", domExc.code, domExc.message, domExc.name)
console.error("An exception occured", domExc.code, domExc.message, domExc.name, domExc)
}
console.error(e)
}

View file

@ -8,7 +8,7 @@ import Translations from "../i18n/Translations";
export class CheckBox extends InputElementMap<number[], boolean> {
constructor(el: (BaseUIElement | string), defaultValue?: boolean) {
super(
new CheckBoxes([Translations.T(el)]),
new CheckBoxes([Translations.W(el)]),
(x0, x1) => x0 === x1,
(t) => t.length > 0,
(x) => (x ? [0] : [])

View file

@ -10,16 +10,15 @@ import Toggle from "./Input/Toggle"
export default class LanguagePicker extends Toggle {
constructor(languages: string[], label: string | BaseUIElement = "") {
console.log("Constructing a language pîcker for languages", languages)
if (languages === undefined || languages.length <= 1) {
super(undefined, undefined, undefined)
return undefined
}
const allLanguages: string[] = used_languages.languages
}else {
const normalPicker = LanguagePicker.dropdownFor(languages, label)
const fullPicker = new Lazy(() => LanguagePicker.dropdownFor(allLanguages, label))
super(fullPicker, normalPicker, Locale.showLinkToWeblate)
const allLanguages: string[] = used_languages.languages
}
}
private static dropdownFor(languages: string[], label: string | BaseUIElement): BaseUIElement {

View file

@ -248,9 +248,8 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
)
editElements.push(
new VariableUiElement(
state.featureSwitchIsDebugging.map((isDebugging) => {
if (isDebugging) {
Toggle.If(state.featureSwitchIsDebugging,
() => {
const config_all_tags: TagRenderingConfig = new TagRenderingConfig(
{ render: "{all_tags()}" },
""
@ -271,7 +270,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
"This is layer " + layerConfig.id,
])
}
})
)
)

View file

@ -43,6 +43,38 @@ export default class TagApplyButton implements AutoAction {
public readonly example =
"`{tag_apply(survey_date=$_now:date, Surveyed today!)}`, `{tag_apply(addr:street=$addr:street, Apply the address, apply_icon.svg, _closest_osm_id)"
/**
* Parses a tag specification
*
* TagApplyButton.parseTagSpec("key=value;key0=value0") // => [["key","value"],["key0","value0"]]
*
* // Should handle escaped ";"
* TagApplyButton.parseTagSpec("key=value;key0=value0\\;value1") // => [["key","value"],["key0","value0;value1"]]
*/
private static parseTagSpec(spec: string): [string, string][]{
const tgsSpec : [string, string][] = []
while(spec.length > 0){
const [part] = spec.match(/((\\;)|[^;])*/)
spec = spec.substring(part.length + 1) // +1 to remove the pending ';' as well
const kv = part.split("=").map((s) => s.trim().replace("\\;",";"))
if (kv.length == 2) {
tgsSpec.push(<[string, string]> kv)
}else if (kv.length < 2) {
throw "Invalid key spec: no '=' found in " + spec
}else{
throw "Invalid key spec: multiple '=' found in " + spec
}
}
for (const spec of tgsSpec) {
if (spec[0].endsWith(":")) {
throw "The key for a tag specification for import or apply ends with ':'. The theme author probably wrote key:=otherkey instead of key=$otherkey"
}
}
return tgsSpec
}
public static generateTagsToApply(spec: string, tagSource: Store<any>): Store<Tag[]> {
// Check whether we need to look up a single value
@ -51,19 +83,7 @@ export default class TagApplyButton implements AutoAction {
spec = tagSource.data[spec.replace("$", "")]
}
const tgsSpec = spec.split(";").map((spec) => {
const kv = spec.split("=").map((s) => s.trim())
if (kv.length != 2) {
throw "Invalid key spec: multiple '=' found in " + spec
}
return kv
})
for (const spec of tgsSpec) {
if (spec[0].endsWith(":")) {
throw "A tag specification for import or apply ends with ':'. The theme author probably wrote key:=otherkey instead of key=$otherkey"
}
}
const tgsSpec = TagApplyButton.parseTagSpec(spec)
return tagSource.map((tags) => {
const newTags: Tag[] = []

View file

@ -41,7 +41,7 @@ export default class Translations {
* translation.textFor("nl") // => "Nederlands"
*
*/
static T(t: string | any, context = undefined): TypedTranslation<object> {
static T(t: string | undefined | null | Translation | TypedTranslation<object>, context = undefined): TypedTranslation<object> {
if (t === undefined || t === null) {
return undefined
}
@ -51,7 +51,7 @@ export default class Translations {
if (typeof t === "string") {
return new TypedTranslation<object>({ "*": t }, context)
}
if (t.render !== undefined) {
if (t["render"] !== undefined) {
const msg =
"Creating a translation, but this object contains a 'render'-field. Use the translation directly"
console.error(msg, t)

View file

@ -1,7 +1,7 @@
{
"id": "filters",
"description": "This layer acts as library for common filters",
"mapRendering": [],
"mapRendering": null,
"source": {
"osmTags": "id~*"
},

View file

@ -1,7 +1,7 @@
[
{
"path": "hotel.svg",
"license": "",
"license": "CC0",
"authors": [
"Andy Allan",
"Michael Glanznig",

View file

@ -58,6 +58,7 @@
}
],
"tagRenderings": [
"images",
{
"id": "kerb-type",
"question": {

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="500"
height="500"
version="1.1"
id="svg20"
sodipodi:docname="icon.svg"
inkscape:version="1.2.1 (0f2f062aeb, 2022-09-21, custom)"
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="defs24" />
<sodipodi:namedview
id="namedview22"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
showgrid="false"
showguides="true"
inkscape:zoom="0.6971062"
inkscape:cx="755.98238"
inkscape:cy="65.269826"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg20">
<sodipodi:guide
position="4.4436256,249.99444"
orientation="0,-1"
id="guide132"
inkscape:locked="false" />
</sodipodi:namedview>
<path
d="M 34.421203,251.09539 H 466.71474 m -356.64217,-86.4587 -86.458705,86.4587 86.458705,86.45871 m 280.9908,-172.91741 86.45871,86.4587 -86.45871,86.45871"
stroke="#000000"
stroke-width="43.2294"
stroke-linejoin="round"
stroke-linecap="round"
fill="none"
id="path18" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,8 @@
[
{
"path": "icon.svg",
"license": "CC0; trivial",
"authors": [],
"sources": []
}
]

View file

@ -0,0 +1,271 @@
{
"id": "width",
"description": {
"nl": "<h3>De straat is opgebruikt</h3> <p>Er is steeds meer druk op de openbare ruimte. Voetgangers, fietsers, steps, auto's, bussen, bestelwagens, buggies, cargobikes, ... willen allemaal hun deel van de openbare ruimte en de straat.</p> <p>In deze studie nemen we Brugge onder de loep en kijken we hoe breed elke straat is én hoe breed elke straat zou moeten zijn voor een veilig én vlot verkeer.</p> <h3>Legende</h3> <span style='background: red'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat te smal voor veilig verkeer<br/> <span style='background: #0f0'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat is breed genoeg veilig verkeer<br/> <span style='background: orange'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat zonder voetpad, te smal als ook voetgangers plaats krijgen<br/> <span style='background: lightgrey'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Autoluw, autoloos of enkel plaatselijk verkeer<br/> <br/> <br/> Een gestippelde lijn is een straat waar ook voor fietsers éénrichtingsverkeer geldt.<br/> Klik op een straat om meer informatie te zien."
},
"title": {
"nl": "Straatbreedtes"
},
"mustHaveLanguage": [
"nl"
],
"hideFromOverview": true,
"enableUserBadge": false,
"enableShareScreen": false,
"enableLayers": false,
"enableMoreQuests": false,
"enableSearch": false,
"enableBackgroundLayerSelection": false,
"icon": "./assets/themes/width/icon.svg",
"startLat": 51.20875,
"startLon": 3.22435,
"startZoom": 14,
"widenFactor": 0.05,
"clustering": false,
"lockLocation": [
[
3.2006263732910156,
51.22699040520305
],
[
3.2529830932617188,
51.190748429411705
]
],
"defaultBackgroundId": "Stadia.AlidadeSmoothDark",
"layers": [
{
"id": "street_with_width",
"description": "A layer showing street with corresponding widths + an analysis of what this width is used for",
"name": {
"nl": "Straten met een breedte"
},
"calculatedTags": [
"_car_width:=2 /* The width that a single car needs */",
"_cyclistWidth:=1.5 /* The width a single cyclist needs to be safely overtaken */",
"_pedestrianWidth:=0.75 /* The width a pedestrian needs if sidewalks are missing */",
"_has_left_parking=(feat.properties['parking:lane:left'] ?? feat.properties['parking:lane:both']) === 'parallel'",
"_has_right_parking=(feat.properties['parking:lane:right'] ?? feat.properties['parking:lane:both']) === 'parallel'",
"_has_other_parking= ['parking:lane:left','parking:lane:right','parking:lane:both'].some(key => ['perpendicular','diagonal'].indexOf(feat.properties[key]) >= 0)",
"_parallel_parking_count=feat.get('_has_right_parking') + feat.get('_has_left_parking') /* in javascript logic: true + true == 2*/",
"_width:needed:parking=feat.get('_parallel_parking_count') * feat.get('_car_width')",
"_has_sidewalk_left=['left','both'].indexOf(feat.properties['sidewalk']) >= 0",
"_has_sidewalk_right=['right','both'].indexOf(feat.properties['sidewalk']) >= 0",
"_pedestrian_flows_in_carriageway= 2 - feat.get('_has_sidewalk_left') - feat.get('_has_sidewalk_right')",
"_width:needed:pedestrians=feat.get('_pedestrianWidth') * feat.get('_pedestrian_flows_in_carriageway')",
"_oneway_car=(feat.properties['oneway:motor_vehicle'] ?? feat.properties['oneway']) == 'yes'",
"_width:needed:cars=feat.get('_car_width') * (2 - feat.get('_oneway_car'))",
"_cycling_allowed=feat.properties.bicycle != 'use_sidepath' && feat.properties.bicycle!='no'",
"_oneway_bicycle=((feat.properties['oneway:bicycle'] ?? feat.properties['oneway']) == 'yes') && feat.properties['cycleway'] != 'opposite'",
"_width:needed:cyclists=feat.get('_cycling_allowed') ? (feat.get('_cyclistWidth') * (2 - feat.get('_oneway_bicycle'))) : 0",
"_width:needed:total:=feat.get('_width:needed:cars') + feat.get('_width:needed:parking') + feat.get('_width:needed:cyclists') + feat.get('_width:needed:pedestrians')",
"_width:difference:=feat.get('_width:needed:total') - feat.get('width:carriageway')",
"_width:difference:no_pedestrians:=feat.get('_width:difference') - feat.get('_width:needed:pedestrians')"
],
"minzoom": 12,
"source": {
"osmTags": "width:carriageway~*"
},
"title": {
"render": {
"nl": "{name}"
},
"mappings": [
{
"if": "name=",
"then": {
"nl": "Naamloos segment"
}
}
]
},
"tagRenderings": [
{
"id": "carriageway_width",
"render": "Deze straat is <b>{width:carriageway}m</b> breed",
"question": "Hoe breed is deze straat?",
"freeform": {
"key": "width:carriageway",
"type": "distance",
"helperArgs": [
21,
"map"
]
}
},
{
"id": "too_little_width",
"render": "Deze straat heeft <span class='alert'>{_width:difference}m</span> te weinig. De ruimte die nodig zou zijn is:",
"mappings": [
{
"if": {
"or": [
"_width:difference~-.*",
"_width:difference=0"
]
},
"then": "Deze straat is breed genoeg:"
}
]
},
{
"id": "needed_for_cars",
"render": "<b>{_width:needed:cars}m</b> voor het autoverkeer",
"mappings": [
{
"if": "oneway=yes",
"then": "<b>{_width:needed:cars}m</b> voor het éénrichtings-autoverkeer"
},
{
"if": "oneway=no",
"then": "<b>{_width:needed:cars}m</b> voor het tweerichtings-autoverkeer"
}
]
},
{
"id": "needed_for_parking",
"render": "<b>{_width:needed:parking}m</b> voor het geparkeerde wagens",
"condition": "_width:needed:parking!=0"
},
{
"id": "needed_for_cyclists",
"render": "<b>{_width:needed:cyclists}m</b> voor fietsers",
"mappings": [
{
"if": "bicycle=use_sidepath",
"then": "Fietsers hebben hier een vrijliggend fietspad en worden dus niet meegerekend"
},
{
"if": "oneway:bicycle=yes",
"then": "<b>{_width:needed:cyclists}m</b> voor fietsers die met de rijrichting mee moeten"
}
]
},
{
"id": "needed_for_pedestrians",
"render": "<b>{_width:needed:pedestrians}m</b> voor voetgangers",
"condition": "_width:needed:pedestrians!=0",
"mappings": [
{
"if": {
"or": [
"sidewalk=none",
"sidewalk=no"
]
},
"then": "<b>{_width:needed:pedestrians}m</b> voor voetgangers: er zijn hier geen voetpaden"
},
{
"if": {
"or": [
"sidewalk=left",
"sidewalk=right"
]
},
"then": "<b>{_width:needed:pedestrians}m</b> voor voetgangers: er is slechts aan één kant een voetpad"
}
]
},
{
"id": "total_width_needed",
"render": "<span style='border: 1px solid black; border-radius: 0.5em; padding: 0.25em;'><b>{_width:needed:total}m</b> nodig in het totaal</span>"
},
{
"id": "has_sidewalks",
"condition": "id=disabled",
"question": {
"nl": "Heeft deze straat voetpaden?"
},
"mappings": [
{
"if": "sidewalk=both",
"then": {
"nl": "Voetpad aan beide zijden"
}
},{
"if": "sidewalk=none",
"then": {
"nl": "Heeft géén voetpaden"
}
},{
"if": "sidewalk=left",
"then": {
"nl": "Voetpad aan de linkerkant"
}
},
{
"if": "sidewalk=right",
"then": {
"nl": "Voetpad aan de rechterzijde"
}
}
]
}
],
"mapRendering": [
{
"location": [
"point"
],
"icon": "./assets/themes/width/icon.svg",
"iconSize": "40,40,center"
},
{
"width": "4",
"color": {
"render": "#00f",
"mappings": [
{
"if": {
"or": [
"access=destination",
"highway=pedestrian",
"motor_vehicle=no",
"motor_vehicle=destination"
]
},
"then": "lightgrey"
},
{
"if": {
"and": [
"_width:difference!~-.*",
"_width:difference:no_pedestrians~-.*"
]
},
"then": "orange"
},
{
"if": "_width:difference~-.*",
"then": "#0f0"
},
{
"if": "_width:difference!~-.*",
"then": "#f00"
}
]
},
"dashArray": {
"render": "",
"mappings": [
{
"if": {
"and": [
"oneway=yes",
{
"or": [
"oneway:bicycle=yes",
"oneway:bicycle="
]
}
]
},
"then": "5 6"
}
]
}
}
]
}
]
}

View file

@ -261,7 +261,9 @@ function main(args: string[]) {
mkdirSync("./assets/generated")
}
let contents = ScriptUtils.readDirRecSync("./assets").filter(
let contents = ScriptUtils.readDirRecSync("./assets")
.filter(p => !p.startsWith("./assets/templates/"))
.filter(
(entry) => entry.indexOf("./assets/generated") != 0
)
let licensePaths = contents.filter((entry) => entry.indexOf("license_info.json") >= 0)