Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-02-15 17:48:26 +01:00
commit f0823f4c4d
524 changed files with 18747 additions and 8546 deletions

View file

@ -8,7 +8,7 @@ import { RasterLayerPolygon, RasterLayerUtils } from "../../Models/RasterLayers"
*/
export default class BackgroundLayerResetter {
constructor(
currentBackgroundLayer: UIEventSource<RasterLayerPolygon>,
currentBackgroundLayer: UIEventSource<RasterLayerPolygon | undefined>,
availableLayers: Store<RasterLayerPolygon[]>
) {
if (Utils.runningFromConsole) {

View file

@ -1,4 +1,4 @@
import { UIEventSource } from "../UIEventSource"
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { QueryParameters } from "../Web/QueryParameters"
@ -15,6 +15,7 @@ import { QueryParameters } from "../Web/QueryParameters"
export default class InitialMapPositioning {
public zoom: UIEventSource<number>
public location: UIEventSource<{ lon: number; lat: number }>
public useTerrain: Store<boolean>
constructor(layoutToUse: LayoutConfig) {
function localStorageSynced(
key: string,
@ -55,10 +56,11 @@ export default class InitialMapPositioning {
)
this.location = new UIEventSource({ lon: lon.data, lat: lat.data })
// Note: this syncs only in one direction
this.location.addCallbackD((loc) => {
lat.setData(loc.lat)
lon.setData(loc.lon)
})
// Note: this syncs only in one direction
this.useTerrain = new ImmutableStore<boolean>(layoutToUse.enableTerrain)
}
}

View file

@ -12,20 +12,21 @@ import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
export default class TitleHandler {
constructor(
selectedElement: Store<Feature>,
selectedLayer: Store<LayerConfig>,
allElements: FeaturePropertiesStore,
state: SpecialVisualizationState
) {
const currentTitle: Store<string> = selectedElement.map(
(selected) => {
const defaultTitle = state.layout?.title?.txt ?? "MapComplete"
if (selected === undefined || selectedLayer.data === undefined) {
if (selected === undefined) {
return defaultTitle
}
const layer = state.layout.getMatchingLayer(selected.properties)
if (layer === undefined) {
return defaultTitle
}
const tags = selected.properties
const layer = selectedLayer.data
if (layer.title === undefined) {
return defaultTitle
}
@ -43,7 +44,7 @@ export default class TitleHandler {
defaultTitle
)
},
[Locale.language, selectedLayer]
[Locale.language]
)
currentTitle.addCallbackAndRunD((title) => {

View file

@ -15,7 +15,7 @@ export interface ExtraFuncParams {
*/
getFeaturesWithin: (
layerId: string,
bbox: BBox,
bbox: BBox
) => Feature<Geometry, Record<string, string>>[][]
getFeatureById: (id: string) => Feature<Geometry, Record<string, string>>
}
@ -71,7 +71,7 @@ class EnclosingFunc implements ExtraFunction {
if (
GeoOperations.completelyWithin(
<Feature>feat,
<Feature<Polygon | MultiPolygon, any>>otherFeature,
<Feature<Polygon | MultiPolygon, any>>otherFeature
)
) {
result.push({ feat: otherFeature })
@ -162,7 +162,7 @@ class IntersectionFunc implements ExtraFunction {
for (const otherFeature of otherFeatures) {
const intersections = GeoOperations.LineIntersections(
feat,
<Feature<any, Record<string, string>>>otherFeature,
<Feature<any, Record<string, string>>>otherFeature
)
if (intersections.length === 0) {
continue
@ -192,7 +192,7 @@ class DistanceToFunc implements ExtraFunction {
// Feature._lon and ._lat is conveniently place by one of the other metatags
return GeoOperations.distanceBetween(
[arg0, lat],
GeoOperations.centerpointCoordinates(feature),
GeoOperations.centerpointCoordinates(feature)
)
}
if (typeof arg0 === "string") {
@ -207,7 +207,7 @@ class DistanceToFunc implements ExtraFunction {
// arg0 is probably a geojsonfeature
return GeoOperations.distanceBetween(
GeoOperations.centerpointCoordinates(arg0),
GeoOperations.centerpointCoordinates(feature),
GeoOperations.centerpointCoordinates(feature)
)
}
}
@ -253,29 +253,29 @@ class ClosestNObjectFunc implements ExtraFunction {
params: ExtraFuncParams,
feature: any,
features: string | string[] | Feature[],
options?: { maxFeatures?: number; uniqueTag?: string | undefined; maxDistance?: number },
options?: { maxFeatures?: number; uniqueTag?: string | undefined; maxDistance?: number }
): { feat: any; distance: number }[] {
const maxFeatures = options?.maxFeatures ?? 1
const maxDistance = options?.maxDistance ?? 500
const uniqueTag: string | undefined = options?.uniqueTag
let allFeatures: Feature[][]
if (typeof features === "string") {
features = [features]
} else {
allFeatures = []
for (const spec of features) {
if (typeof spec === "string") {
const name = spec
const bbox = GeoOperations.bbox(
GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance),
)
const coors = <[number, number][]>bbox.geometry.coordinates
allFeatures.push(...params.getFeaturesWithin(name, new BBox(coors)))
} else {
allFeatures.push([spec])
}
}
let allFeatures: Feature[][] = []
for (const spec of features) {
if (typeof spec === "string") {
const name = spec
const bbox = GeoOperations.bbox(
GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance)
)
const coors = <[number, number][]>bbox.geometry.coordinates
allFeatures.push(...params.getFeaturesWithin(name, new BBox(coors)))
} else {
allFeatures.push([spec])
}
}
if (features === undefined) {
return
}
@ -283,7 +283,7 @@ class ClosestNObjectFunc implements ExtraFunction {
const selfCenter = GeoOperations.centerpointCoordinates(feature)
let closestFeatures: { feat: any; distance: number }[] = []
for (const feats of allFeatures) {
for (const feats of allFeatures ?? []) {
for (const otherFeature of feats) {
if (otherFeature.properties === undefined) {
console.warn("OtherFeature does not have properties:", otherFeature)
@ -296,14 +296,14 @@ class ClosestNObjectFunc implements ExtraFunction {
}
const distance = GeoOperations.distanceBetween(
GeoOperations.centerpointCoordinates(otherFeature),
selfCenter,
selfCenter
)
if (distance === undefined || distance === null || isNaN(distance)) {
console.error(
"Could not calculate the distance between",
feature,
"and",
otherFeature,
otherFeature
)
throw "Undefined distance!"
}
@ -313,7 +313,7 @@ class ClosestNObjectFunc implements ExtraFunction {
"Got a suspiciously zero distance between",
otherFeature,
"and self-feature",
feature,
feature
)
}
@ -347,7 +347,7 @@ class ClosestNObjectFunc implements ExtraFunction {
const uniqueTagsMatch =
otherFeature.properties[uniqueTag] !== undefined &&
closestFeature.feat.properties[uniqueTag] ===
otherFeature.properties[uniqueTag]
otherFeature.properties[uniqueTag]
if (uniqueTagsMatch) {
targetIndex = -1
if (closestFeature.distance > distance) {
@ -440,7 +440,7 @@ class GetParsed implements ExtraFunction {
return parsed
} catch (e) {
console.warn(
"Could not parse property " + key + " due to: " + e + ", the value is " + value,
"Could not parse property " + key + " due to: " + e + ", the value is " + value
)
return undefined
}
@ -464,10 +464,10 @@ export class ExtraFunctions {
]),
"To enable this feature, add a field `calculatedTags` in the layer object, e.g.:",
"````",
"\"calculatedTags\": [",
" \"_someKey=javascript-expression (lazy execution)\",",
" \"_some_other_key:=javascript expression (strict execution)",
" \"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator\",",
'"calculatedTags": [',
' "_someKey=javascript-expression (lazy execution)",',
' "_some_other_key:=javascript expression (strict execution)',
' "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",',
" \"_distanceCloserThen3Km=distanceTo(feat)( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ",
" ]",
"````",
@ -506,7 +506,7 @@ export class ExtraFunctions {
]
public static constructHelpers(
params: ExtraFuncParams,
params: ExtraFuncParams
): Record<ExtraFuncType, (feature: Feature) => Function> {
const record: Record<string, (feature: GeoJSONFeature) => Function> = {}
for (const f of ExtraFunctions.allFuncs) {

View file

@ -159,7 +159,6 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
public removeFavourite(feature: Feature, tags?: UIEventSource<Record<string, string>>) {
const id = feature.properties.id.replace("/", "-")
const pref = this._osmConnection.GetPreference("favourite-" + id)
this._osmConnection.preferencesHandler.removeAllWithPrefix("mapcomplete-favourite-" + id)
if (tags) {
delete tags.data._favourite

View file

@ -53,7 +53,6 @@ export default class LayoutSource extends FeatureSourceMerger {
/*
const overpassSource = LayoutSource.setupOverpass(
backend,
osmLayers,
bounds,
zoom,
@ -144,7 +143,6 @@ export default class LayoutSource extends FeatureSourceMerger {
}
private static setupOverpass(
backend: string,
osmLayers: LayerConfig[],
bounds: Store<BBox>,
zoom: Store<number>,

View file

@ -48,7 +48,7 @@ export default class NearbyFeatureSource implements FeatureSource {
flayer.layerDef.minzoom,
flayer.isDisplayed
)
calcSource.addCallbackAndRunD((features) => {
calcSource.addCallbackAndRunD(() => {
this.update()
})
this._allSources.push(calcSource)

View file

@ -103,7 +103,7 @@ export default class OverpassFeatureSource implements FeatureSource {
if (!result) {
return
}
const [bounds, date, updatedLayers] = result
const [bounds, _, __] = result
this._lastQueryBBox = bounds
}

View file

@ -6,11 +6,14 @@ import { BBox } from "../../BBox"
export interface SnappingOptions {
/**
* If the distance is bigger then this amount, don't snap.
* If the distance to the line is bigger then this amount, don't snap.
* In meter
*/
maxDistance: number
/**
* If set to true, no value will be given if no snapping was made
*/
allowUnsnapped?: false | boolean
/**

View file

@ -156,7 +156,7 @@ export class GeoOperations {
const intersection = GeoOperations.calculateIntersection(
feature,
otherFeature,
featureBBox,
featureBBox
)
if (intersection === null) {
continue
@ -195,7 +195,7 @@ export class GeoOperations {
console.error(
"Could not correctly calculate the overlap of ",
feature,
": unsupported type",
": unsupported type"
)
return result
}
@ -224,7 +224,7 @@ export class GeoOperations {
*/
public static inside(
pointCoordinate: [number, number] | Feature<Point>,
feature: Feature,
feature: Feature
): boolean {
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
@ -302,7 +302,7 @@ export class GeoOperations {
*/
public static nearestPoint(
way: Feature<LineString>,
point: [number, number],
point: [number, number]
): Feature<
Point,
{
@ -324,11 +324,11 @@ export class GeoOperations {
public static forceLineString(way: Feature<LineString | Polygon>): Feature<LineString>
public static forceLineString(
way: Feature<MultiLineString | MultiPolygon>,
way: Feature<MultiLineString | MultiPolygon>
): Feature<MultiLineString>
public static forceLineString(
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
): Feature<LineString | MultiLineString> {
if (way.geometry.type === "Polygon") {
way = { ...way }
@ -345,11 +345,21 @@ export class GeoOperations {
return <any>way
}
public static toCSV(features: Feature[] | FeatureCollection): string {
public static toCSV(
features: Feature[] | FeatureCollection,
options?: {
ignoreTags?: RegExp
}
): string {
const headerValuesSeen = new Set<string>()
const headerValuesOrdered: string[] = []
function addH(key) {
function addH(key: string) {
if (options?.ignoreTags) {
if (key.match(options.ignoreTags)) {
return
}
}
if (!headerValuesSeen.has(key)) {
headerValuesSeen.add(key)
headerValuesOrdered.push(key)
@ -448,7 +458,7 @@ export class GeoOperations {
*/
public static LineIntersections(
feature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
otherFeature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
otherFeature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
): [number, number][] {
return turf
.lineIntersect(feature, otherFeature)
@ -485,7 +495,7 @@ export class GeoOperations {
locations:
| Feature<LineString>
| Feature<Point, { date?: string; altitude?: number | string }>[],
title?: string,
title?: string
) {
title = title?.trim()
if (title === undefined || title === "") {
@ -506,7 +516,7 @@ export class GeoOperations {
type: "Point",
coordinates: p,
},
},
}
)
}
for (const l of locationsWithMeta) {
@ -521,7 +531,7 @@ export class GeoOperations {
trackPoints.push(trkpt)
}
const header =
"<gpx version=\"1.1\" creator=\"mapcomplete.org\" xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">"
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
return (
header +
"\n<name>" +
@ -539,7 +549,7 @@ export class GeoOperations {
*/
public static toGpxPoints(
locations: Feature<Point, { date?: string; altitude?: number | string }>[],
title?: string,
title?: string
) {
title = title?.trim()
if (title === undefined || title === "") {
@ -560,7 +570,7 @@ export class GeoOperations {
trackPoints.push(trkpt)
}
const header =
"<gpx version=\"1.1\" creator=\"mapcomplete.org\" xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">"
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
return (
header +
"\n<name>" +
@ -648,7 +658,7 @@ export class GeoOperations {
},
},
distanceMeter,
{ units: "meters" },
{ units: "meters" }
).geometry.coordinates
}
@ -683,7 +693,7 @@ export class GeoOperations {
*/
static completelyWithin(
feature: Feature<Geometry, any>,
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon, any>,
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon, any>
): boolean {
return booleanWithin(feature, possiblyEnclosingFeature)
}
@ -716,8 +726,11 @@ export class GeoOperations {
}
if (toSplit.geometry.type === "MultiLineString") {
const lines: Feature<LineString>[][] = toSplit.geometry.coordinates.map(coordinates =>
turf.lineSplit(<LineString> {type: "LineString", coordinates}, boundary).features )
const lines: Feature<LineString>[][] = toSplit.geometry.coordinates.map(
(coordinates) =>
turf.lineSplit(<LineString>{ type: "LineString", coordinates }, boundary)
.features
)
const splitted: Feature<LineString>[] = [].concat(...lines)
const kept: Feature<LineString>[] = []
for (const f of splitted) {
@ -728,7 +741,6 @@ export class GeoOperations {
f.properties = { ...toSplit.properties }
kept.push(f)
}
console.log(">>>", {lines, splitted, kept})
return kept
}
if (toSplit.geometry.type === "Polygon" || toSplit.geometry.type == "MultiPolygon") {
@ -756,7 +768,14 @@ export class GeoOperations {
*/
public static featureToCoordinateWithRenderingType(
feature: Feature,
location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string,
location:
| "point"
| "centroid"
| "start"
| "end"
| "projected_centerpoint"
| "polygon_centerpoint"
| string
): [number, number] | undefined {
switch (location) {
case "point":
@ -769,6 +788,11 @@ export class GeoOperations {
return undefined
}
return GeoOperations.centerpointCoordinates(feature)
case "polygon_centerpoint":
if (feature.geometry.type === "Polygon") {
return GeoOperations.centerpointCoordinates(feature)
}
return undefined
case "projected_centerpoint":
if (
feature.geometry.type === "LineString" ||
@ -777,7 +801,7 @@ export class GeoOperations {
const centerpoint = GeoOperations.centerpointCoordinates(feature)
const projected = GeoOperations.nearestPoint(
<Feature<LineString>>feature,
centerpoint,
centerpoint
)
return <[number, number]>projected.geometry.coordinates
}
@ -954,7 +978,7 @@ export class GeoOperations {
* GeoOperations.bearingToHuman(46) // => "NE"
*/
public static bearingToHuman(
bearing: number,
bearing: number
): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" {
while (bearing < 0) {
bearing += 360
@ -966,14 +990,21 @@ export class GeoOperations {
}
/**
* GeoOperations.bearingToHuman(0) // => "N"
* GeoOperations.bearingToHuman(-10) // => "N"
* GeoOperations.bearingToHuman(-180) // => "S"
* GeoOperations.bearingToHuman(181) // => "S"
* GeoOperations.bearingToHuman(46) // => "NE"
* GeoOperations.bearingToHumanRelative(-207) // => "sharp_right"
* GeoOperations.bearingToHumanRelative(-199) // => "behind"
* GeoOperations.bearingToHumanRelative(-180) // => "behind"
* GeoOperations.bearingToHumanRelative(-10) // => "straight"
* GeoOperations.bearingToHumanRelative(0) // => "straight"
* GeoOperations.bearingToHumanRelative(181) // => "behind"
* GeoOperations.bearingToHumanRelative(40) // => "slight_right"
* GeoOperations.bearingToHumanRelative(46) // => "slight_right"
* GeoOperations.bearingToHumanRelative(95) // => "right"
* GeoOperations.bearingToHumanRelative(140) // => "sharp_right"
* GeoOperations.bearingToHumanRelative(158) // => "behind"
*
*/
public static bearingToHumanRelative(
bearing: number,
bearing: number
):
| "straight"
| "slight_right"
@ -998,9 +1029,11 @@ export class GeoOperations {
* const merged = GeoOperations.attemptLinearize(f)
* merged.geometry.coordinates // => [[3.2176208,51.21760169669458],[3.217198532946432,51.218067], [3.216807134449482,51.21849812105347],[3.2164304037883706,51.2189272]]
*/
static attemptLinearize(multiLineStringFeature: Feature<MultiLineString>): Feature<LineString | MultiLineString> {
static attemptLinearize(
multiLineStringFeature: Feature<MultiLineString>
): Feature<LineString | MultiLineString> {
const coors = multiLineStringFeature.geometry.coordinates
if(coors.length === 0) {
if (coors.length === 0) {
console.error(multiLineStringFeature.geometry)
throw "Error: got degenerate multilinestring"
}
@ -1014,7 +1047,12 @@ export class GeoOperations {
}
const jLast = coors[j].at(-1)
if (!(Math.abs(iFirst[0] - jLast[0]) < 0.000001 && Math.abs(iFirst[1] - jLast[1]) < 0.0000001)) {
if (
!(
Math.abs(iFirst[0] - jLast[0]) < 0.000001 &&
Math.abs(iFirst[1] - jLast[1]) < 0.0000001
)
) {
continue
}
coors[j].splice(coors.length - 1, 1)
@ -1023,7 +1061,7 @@ export class GeoOperations {
continue outer
}
}
if(coors.length === 0) {
if (coors.length === 0) {
throw "No more coordinates found"
}
@ -1053,12 +1091,12 @@ export class GeoOperations {
private static pointInPolygonCoordinates(
x: number,
y: number,
coordinates: [number, number][][],
coordinates: [number, number][][]
): boolean {
const inside = GeoOperations.pointWithinRing(
x,
y,
/*This is the outer ring of the polygon */ coordinates[0],
/*This is the outer ring of the polygon */ coordinates[0]
)
if (!inside) {
return false
@ -1067,7 +1105,7 @@ export class GeoOperations {
const inHole = GeoOperations.pointWithinRing(
x,
y,
coordinates[i], /* These are inner rings, aka holes*/
coordinates[i] /* These are inner rings, aka holes*/
)
if (inHole) {
return false
@ -1105,7 +1143,7 @@ export class GeoOperations {
feature,
otherFeature,
featureBBox: BBox,
otherFeatureBBox?: BBox,
otherFeatureBBox?: BBox
): number {
if (feature.geometry.type === "LineString") {
otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature)
@ -1154,7 +1192,7 @@ export class GeoOperations {
let intersection = turf.lineSlice(
turf.point(intersectionPointsArray[0]),
turf.point(intersectionPointsArray[1]),
feature,
feature
)
if (intersection == null) {
@ -1175,7 +1213,7 @@ export class GeoOperations {
otherFeature,
feature,
otherFeatureBBox,
featureBBox,
featureBBox
)
}
@ -1195,7 +1233,7 @@ export class GeoOperations {
console.log("Applying fallback intersection...")
const intersection = turf.intersect(
turf.truncate(feature),
turf.truncate(otherFeature),
turf.truncate(otherFeature)
)
if (intersection == null) {
return null

View file

@ -133,7 +133,7 @@ export class Mapillary extends ImageProvider {
return [this.PrepareUrlAsync(key, value)]
}
public async DownloadAttribution(url: string): Promise<LicenseInfo> {
public async DownloadAttribution(_: string): Promise<LicenseInfo> {
const license = new LicenseInfo()
license.artist = undefined
license.license = "CC BY-SA 4.0"
@ -155,7 +155,6 @@ export class Mapillary extends ImageProvider {
Constants.mapillary_client_token_v4
const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60)
const url = <string>response["thumb_1024_url"]
console.log(response)
const url_hd = <string>response["thumb_original_url"]
return {
id: "" + mapillaryId,

View file

@ -1,6 +1,5 @@
import ImageProvider, { ProvidedImage } from "./ImageProvider"
import BaseUIElement from "../../UI/BaseUIElement"
import Svg from "../../Svg"
import { Utils } from "../../Utils"
import { LicenseInfo } from "./LicenseInfo"
import Wikimedia from "../Web/Wikimedia"

View file

@ -15,11 +15,11 @@ export default class Maproulette {
public static readonly STATUS_MEANING = {
0: "Open",
1: "Fixed",
2: "False positive",
2: "False_positive",
3: "Skipped",
4: "Deleted",
5: "Already fixed",
6: "Too hard",
6: "Too_Hard",
9: "Disabled",
}
public static singleton = new Maproulette()

View file

@ -1,7 +1,6 @@
import ChangeTagAction from "./ChangeTagAction"
import { Tag } from "../../Tags/Tag"
import OsmChangeAction from "./OsmChangeAction"
import { Changes } from "../Changes"
import { ChangeDescription } from "./ChangeDescription"
import { Store } from "../../UIEventSource"
@ -40,7 +39,7 @@ export default class LinkImageAction extends OsmChangeAction {
protected CreateChangeDescriptions(): Promise<ChangeDescription[]> {
let key = this._proposedKey
let i = 0
const currentTags = this._currentTags.data
const currentTags: Record<string, string> = this._currentTags.data
const url = this._url
while (currentTags[key] !== undefined && currentTags[key] !== url) {
key = this._proposedKey + ":" + i

View file

@ -539,7 +539,7 @@ export class Changes {
openChangeset
)
console.log("Upload successfull!")
console.log("Upload successful!")
return true
}

View file

@ -416,7 +416,7 @@ export class OsmConnection {
): Promise<{ id: number }> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually uploading GPX ", gpx)
return new Promise<{ id: number }>((ok, error) => {
return new Promise<{ id: number }>((ok) => {
window.setTimeout(
() => ok({ id: Math.floor(Math.random() * 1000) }),
Math.random() * 5000

View file

@ -615,7 +615,7 @@ export default class SimpleMetaTaggers {
isLazy: true,
includesDates: true,
},
(feature, layer, tagsStore) => {
(feature) => {
Utils.AddLazyProperty(feature.properties, "_last_edit:passed_time", () => {
const lastEditTimestamp = new Date(
feature.properties["_last_edit:timestamp"]

View file

@ -73,7 +73,6 @@ export default class UserRelatedState {
constructor(
osmConnection: OsmConnection,
availableLanguages?: string[],
layout?: LayoutConfig,
featureSwitches?: FeatureSwitchState,
mapProperties?: MapProperties
@ -365,6 +364,11 @@ export default class UserRelatedState {
[translationMode]
)
this.mangroveIdentity.getKeyId().addCallbackAndRun(kid => {
amendedPrefs.data["mangrove_kid"] = kid
amendedPrefs.ping()
})
const usersettingMetaTagging = new ThemeMetaTagging()
osmConnection.userDetails.addCallback((userDetails) => {
for (const k in userDetails) {

View file

@ -4,6 +4,7 @@ import { TagUtils } from "./TagUtils"
import { Tag } from "./Tag"
import { RegexTag } from "./RegexTag"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
export class And extends TagsFilter {
public and: TagsFilter[]
@ -429,4 +430,8 @@ export class And extends TagsFilter {
f(this)
this.and.forEach((sub) => sub.visit(f))
}
asMapboxExpression(): ExpressionSpecification {
return ["all", ...this.and.map(t => t.asMapboxExpression())]
}
}

View file

@ -1,6 +1,7 @@
import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { Tag } from "./Tag"
import { ExpressionSpecification } from "maplibre-gl"
export default class ComparingTag implements TagsFilter {
private readonly _key: string
@ -12,7 +13,7 @@ export default class ComparingTag implements TagsFilter {
key: string,
predicate: (value: string | undefined) => boolean,
representation: "<" | ">" | "<=" | ">=",
boundary: string
boundary: string,
) {
this._key = key
this._predicate = predicate
@ -20,11 +21,11 @@ export default class ComparingTag implements TagsFilter {
this._boundary = boundary
}
asChange(properties: Record<string, string>): { k: string; v: string }[] {
asChange(_: Record<string, string>): { k: string; v: string }[] {
throw "A comparable tag can not be used to be uploaded to OSM"
}
asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) {
asHumanString() {
return this._key + this._representation + this._boundary
}
@ -125,4 +126,8 @@ export default class ComparingTag implements TagsFilter {
visit(f: (TagsFilter) => void) {
f(this)
}
asMapboxExpression(): ExpressionSpecification {
return [this._representation, ["get", this._key], this._boundary]
}
}

View file

@ -2,6 +2,7 @@ import { TagsFilter } from "./TagsFilter"
import { TagUtils } from "./TagUtils"
import { And } from "./And"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
export class Or extends TagsFilter {
public or: TagsFilter[]
@ -288,4 +289,8 @@ export class Or extends TagsFilter {
f(this)
this.or.forEach((t) => t.visit(f))
}
asMapboxExpression(): ExpressionSpecification {
return ["any", ...this.or.map(t => t.asMapboxExpression())]
}
}

View file

@ -1,6 +1,7 @@
import { Tag } from "./Tag"
import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
export class RegexTag extends TagsFilter {
public readonly key: RegExp | string
@ -357,4 +358,11 @@ export class RegexTag extends TagsFilter {
visit(f: (TagsFilter) => void) {
f(this)
}
asMapboxExpression(): ExpressionSpecification {
if(typeof this.key=== "string" && typeof this.value === "string" ) {
return [this.invert ? "!=" : "==", ["get",this.key], this.value]
}
throw "TODO"
}
}

View file

@ -2,6 +2,7 @@ import { TagsFilter } from "./TagsFilter"
import { Tag } from "./Tag"
import { Utils } from "../../Utils"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
/**
* The substituting-tag uses the tags of a feature a variables and replaces them.
@ -23,6 +24,10 @@ export default class SubstitutingTag implements TagsFilter {
this._invert = invert
}
asMapboxExpression(): ExpressionSpecification {
throw new Error("Method not implemented.")
}
private static substituteString(template: string, dict: Record<string, string>): string {
for (const k in dict) {
template = template.replace(new RegExp("\\{" + k + "\\}", "g"), dict[k])

View file

@ -1,10 +1,12 @@
import { Utils } from "../../Utils"
import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
export class Tag extends TagsFilter {
public key: string
public value: string
constructor(key: string, value: string) {
super()
this.key = key
@ -63,7 +65,7 @@ export class Tag extends TagsFilter {
asOverpass(): string[] {
if (this.value === "") {
// NOT having this key
return ['[!"' + this.key + '"]']
return ["[!\"" + this.key + "\"]"]
}
return [`["${this.key}"="${this.value}"]`]
}
@ -81,7 +83,7 @@ export class Tag extends TagsFilter {
asHumanString(
linkToWiki?: boolean,
shorten?: boolean,
currentProperties?: Record<string, string>
currentProperties?: Record<string, string>,
) {
let v = this.value
if (typeof v !== "string") {
@ -165,4 +167,16 @@ export class Tag extends TagsFilter {
visit(f: (tagsFilter: TagsFilter) => void) {
f(this)
}
asMapboxExpression(): ExpressionSpecification {
if (this.value === "") {
return [
"any",
["!", ["has", this.key]],
["==", ["get", this.key], ""],
]
}
return ["==", ["get", this.key], this.value]
}
}

View file

@ -1,4 +1,5 @@
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
export abstract class TagsFilter {
abstract asOverpass(): string[]
@ -63,4 +64,6 @@ export abstract class TagsFilter {
* Walks the entire tree, every tagsFilter will be passed into the function once
*/
abstract visit(f: (tagsFilter: TagsFilter) => void)
abstract asMapboxExpression(): ExpressionSpecification
}

View file

@ -306,7 +306,8 @@ export abstract class Store<T> implements Readable<T> {
export class ImmutableStore<T> extends Store<T> {
public readonly data: T
static FALSE = new ImmutableStore<boolean>(false)
static TRUE = new ImmutableStore<boolean>(true)
constructor(data: T) {
super()
this.data = data

View file

@ -5,10 +5,12 @@ import { Feature, Position } from "geojson"
import { GeoOperations } from "../GeoOperations"
export class MangroveIdentity {
public readonly keypair: Store<CryptoKeyPair>
public readonly key_id: Store<string>
private readonly keypair: Store<CryptoKeyPair>
private readonly mangroveIdentity: UIEventSource<string>
private readonly key_id: Store<string>
constructor(mangroveIdentity: UIEventSource<string>) {
this.mangroveIdentity = mangroveIdentity
const key_id = new UIEventSource<string>(undefined)
this.key_id = key_id
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
@ -23,13 +25,7 @@ export class MangroveIdentity {
key_id.setData(pem)
})
try {
if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") {
MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {})
}
} catch (e) {
console.error("Could not create identity: ", e)
}
}
/**
@ -44,14 +40,71 @@ export class MangroveIdentity {
// Identity has been loaded via osmPreferences by now - we don't overwrite
return
}
console.log("Creating a new Mangrove identity!")
identity.setData(JSON.stringify(jwk))
}
/**
* Only called to create a review.
*/
async getKeypair(): Promise<CryptoKeyPair> {
if(this.keypair.data ?? "" === ""){
// We want to create a review, but it seems like no key has been setup at this moment
// We create the key
try {
if (!Utils.runningFromConsole && (this.mangroveIdentity.data ?? "") === "") {
await MangroveIdentity.CreateIdentity(this.mangroveIdentity)
}
} catch (e) {
console.error("Could not create identity: ", e)
}
}
return this.keypair.data
}
getKeyId(): Store<string> {
return this.key_id
}
private allReviewsById : UIEventSource<(Review & {kid: string, signature: string})[]>= undefined
/**
* Gets all reviews that are made for the current identity.
*/
public getAllReviews(): Store<(Review & {kid: string, signature: string})[]>{
if(this.allReviewsById !== undefined){
return this.allReviewsById
}
this.allReviewsById = new UIEventSource( [])
this.key_id.map(pem => {
if(pem === undefined){
return []
}
MangroveReviews.getReviews({
kid: pem
}).then(allReviews => {
this.allReviewsById.setData(allReviews.reviews.map(r => ({
...r, ...r.payload
})))
})
})
return this.allReviewsById
}
addReview(review: Review & {kid, signature}) {
this.allReviewsById?.setData(this.allReviewsById?.data?.concat([review]))
}
}
/**
* Tracks all reviews of a given feature, allows to create a new review
*/
export default class FeatureReviews {
/**
* See https://gitlab.com/open-reviews/mangrove/-/blob/master/servers/reviewer/src/review.rs#L269 and https://github.com/pietervdvn/MapComplete/issues/1775
*/
public static readonly REVIEW_OPINION_MAX_LENGTH = 1000
private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {}
public readonly subjectUri: Store<string>
public readonly average: Store<number | null>
@ -172,23 +225,30 @@ export default class FeatureReviews {
* The given review is uploaded to mangrove.reviews and added to the list of known reviews
*/
public async createReview(review: Omit<Review, "sub">): Promise<void> {
if(review.opinion !== undefined && review.opinion.length > FeatureReviews .REVIEW_OPINION_MAX_LENGTH){
throw "Opinion too long, should be at most "+FeatureReviews.REVIEW_OPINION_MAX_LENGTH+" characters long"
}
const r: Review = {
sub: this.subjectUri.data,
...review,
}
const keypair: CryptoKeyPair = this._identity.keypair.data
const keypair: CryptoKeyPair = await this._identity.getKeypair()
const jwt = await MangroveReviews.signReview(keypair, r)
const kid = await MangroveReviews.publicToPem(keypair.publicKey)
await MangroveReviews.submitReview(jwt)
this._reviews.data.push({
const reviewWithKid = {
...r,
kid,
signature: jwt,
madeByLoggedInUser: new ImmutableStore(true),
})
}
this._reviews.data.push( reviewWithKid)
this._reviews.ping()
this._identity.addReview(reviewWithKid)
}
/**
* Adds given reviews to the 'reviews'-UI-eventsource
* @param reviews
@ -228,7 +288,7 @@ export default class FeatureReviews {
...review,
kid: reviewData.kid,
signature: reviewData.signature,
madeByLoggedInUser: this._identity.key_id.map((user_key_id) => {
madeByLoggedInUser: this._identity.getKeyId().map((user_key_id) => {
return reviewData.kid === user_key_id
}),
})

View file

@ -1,4 +1,3 @@
import exp from "constants"
import { Utils } from "../../Utils"
export interface TagInfoStats {
@ -16,9 +15,8 @@ export interface TagInfoStats {
}
export default class TagInfo {
private readonly _backend: string
public static readonly global = new TagInfo()
private readonly _backend: string
constructor(backend = "https://taginfo.openstreetmap.org/") {
this._backend = backend

View file

@ -122,7 +122,7 @@ export default class ThemeViewStateHashActor {
private loadStateFromHash(hash: string) {
const state = this._state
const parts = hash.split(":")
outer: for (const { toggle, name, showOverOthers, submenu } of state.guistate.allToggles) {
outer: for (const { toggle, name, submenu } of state.guistate.allToggles) {
for (const part of parts) {
if (part === name) {
toggle.setData(true)

View file

@ -0,0 +1,259 @@
import { Feature, Geometry } from "geojson"
import { OH } from "../../UI/OpeningHours/OpeningHours"
import EmailValidator from "../../UI/InputElement/Validators/EmailValidator"
import PhoneValidator from "../../UI/InputElement/Validators/PhoneValidator"
import { CountryCoder } from "latlon2country"
import Constants from "../../Models/Constants"
import { Utils } from "../../Utils"
/**
* Commissioned code, to be kept until 2030
*
* Reads a velopark-json, converts it to a geojson
*/
export default class VeloparkLoader {
private static readonly emailReformatting = new EmailValidator()
private static readonly phoneValidator = new PhoneValidator()
private static readonly coder = new CountryCoder(
Constants.countryCoderEndpoint,
Utils.downloadJson
)
public static convert(veloparkData: VeloparkData): Feature {
console.log("Converting", veloparkData)
const properties: {
"ref:velopark": string
"operator:email"?: string
"operator:phone"?: string
fee?: string
opening_hours?: string
access?: string
maxstay?: string
operator?: string
} = {
"ref:velopark": veloparkData["id"] ?? veloparkData["@id"],
}
for (const k of ["_id", "url", "dateModified", "name", "address"]) {
delete veloparkData[k]
}
VeloparkLoader.cleanup(veloparkData["properties"])
VeloparkLoader.cleanupEmtpy(veloparkData)
properties.operator = veloparkData.operatedBy?.companyName
if (veloparkData.contactPoint?.email) {
properties["operator:email"] = VeloparkLoader.emailReformatting.reformat(
veloparkData.contactPoint?.email
)
}
if (veloparkData.contactPoint?.telephone) {
properties["operator:phone"] = VeloparkLoader.phoneValidator.reformat(
veloparkData.contactPoint?.telephone,
() => "be"
)
}
veloparkData.photos?.forEach((p, i) => {
if (i === 0) {
properties["image"] = p.image
} else {
properties["image:" + i] = p.image
}
})
let geometry = veloparkData.geometry
for (const g of veloparkData["@graph"]) {
VeloparkLoader.cleanup(g)
VeloparkLoader.cleanupEmtpy(g)
if (g.geo[0]) {
geometry = { type: "Point", coordinates: [g.geo[0].longitude, g.geo[0].latitude] }
}
if (
g.maximumParkingDuration?.endsWith("D") &&
g.maximumParkingDuration?.startsWith("P")
) {
const duration = g.maximumParkingDuration.substring(
1,
g.maximumParkingDuration.length - 1
)
properties.maxstay = duration + " days"
}
properties.access = g.publicAccess ?? "yes" ? "yes" : "no"
const prefix = "http://schema.org/"
if (g.openingHoursSpecification) {
const oh = OH.simplify(
g.openingHoursSpecification
.map((spec) => {
const dayOfWeek = spec.dayOfWeek
.substring(prefix.length, prefix.length + 2)
.toLowerCase()
const startHour = spec.opens
const endHour = spec.closes === "23:59" ? "24:00" : spec.closes
const merged = OH.MergeTimes(
OH.ParseRule(dayOfWeek + " " + startHour + "-" + endHour)
)
return OH.ToString(merged)
})
.join("; ")
)
properties.opening_hours = oh
}
if (g.priceSpecification?.[0]) {
properties.fee = g.priceSpecification[0].freeOfCharge ? "no" : "yes"
}
const types = {
"https://data.velopark.be/openvelopark/terms#RegularBicycle": "_",
"https://data.velopark.be/openvelopark/terms#ElectricBicycle":
"capacity:electric_bicycle",
"https://data.velopark.be/openvelopark/terms#CargoBicycle": "capacity:cargo_bike",
}
let totalCapacity = 0
for (let i = (g.allows ?? []).length - 1; i >= 0; i--) {
const capacity = g.allows[i]
const type: string = capacity["@type"]
if (type === undefined) {
console.warn("No type found for", capacity.bicycleType)
continue
}
const count = capacity["amount"]
if (!isNaN(count)) {
totalCapacity += Number(count)
} else {
console.warn("Not a valid number while loading velopark data:", count)
}
if (type !== "_") {
// properties[type] = count
}
g.allows.splice(i, 1)
}
if (totalCapacity > 0) {
properties["capacity"] = totalCapacity
}
}
console.log(JSON.stringify(properties, null, " "))
return { type: "Feature", properties, geometry }
}
private static cleanup(data: any) {
if (!data?.attributes) {
return
}
for (const k of ["NIS_CODE", "name_NL", "name_DE", "name_EN", "name_FR"]) {
delete data.attributes[k]
}
VeloparkLoader.cleanupEmtpy(data)
}
private static cleanupEmtpy(data: any) {
for (const key in data) {
if (data[key] === null) {
delete data[key]
continue
}
if (Object.keys(data[key]).length === 0) {
delete data[key]
}
}
}
}
export interface VeloparkData {
geometry?: Geometry
"@context": any
"@id": string // "https://data.velopark.be/data/NMBS_541",
"@type": "BicycleParkingStation"
dateModified: string
identifier: number
name: [
{
"@value": string
"@language": "nl"
}
]
ownedBy: {
"@id": string
"@type": "BusinessEntity"
companyName: string
}
operatedBy: {
"@type": "BusinessEntity"
companyName: string
}
address: any
hasMap: any
contactPoint: {
"@type": "ContactPoint"
email: string
telephone: string
}
photos: {
"@type": "Photograph"
image: string
}[]
interactionService: {
"@type": "WebSite"
url: string
}
/**
* Contains various extra pieces of data, e.g. services or opening hours
*/
"@graph": [
{
"@type": "https://data.velopark.be/openvelopark/terms#PublicBicycleParking"
openingHoursSpecification: {
"@type": "OpeningHoursSpecification"
/**
* Ends with 'Monday', 'Tuesday', ...
*/
dayOfWeek:
| "http://schema.org/Monday"
| "http://schema.org/Tuesday"
| "http://schema.org/Wednesday"
| "http://schema.org/Thursday"
| "http://schema.org/Friday"
| "http://schema.org/Saturday"
| "http://schema.org/Sunday"
/**
* opens: 00:00 and closes 23:59 for the entire day
*/
opens: string
closes: string
}[]
/**
* P30D = 30 days
*/
maximumParkingDuration: "P30D"
publicAccess: true
totalCapacity: 110
allows: [
{
"@type": "AllowedBicycle"
/* TODO is cargo bikes etc also available?*/
bicycleType:
| string
| "https://data.velopark.be/openvelopark/terms#RegularBicycle"
bicyclesAmount: number
}
]
geo: [
{
"@type": "GeoCoordinates"
latitude: number
longitude: number
}
]
priceSpecification: [
{
"@type": "PriceSpecification"
freeOfCharge: boolean
}
]
}
]
}