forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
f0823f4c4d
524 changed files with 18747 additions and 8546 deletions
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -103,7 +103,7 @@ export default class OverpassFeatureSource implements FeatureSource {
|
|||
if (!result) {
|
||||
return
|
||||
}
|
||||
const [bounds, date, updatedLayers] = result
|
||||
const [bounds, _, __] = result
|
||||
this._lastQueryBBox = bounds
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -539,7 +539,7 @@ export class Changes {
|
|||
openChangeset
|
||||
)
|
||||
|
||||
console.log("Upload successfull!")
|
||||
console.log("Upload successful!")
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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())]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}),
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
259
src/Logic/Web/VeloparkLoader.ts
Normal file
259
src/Logic/Web/VeloparkLoader.ts
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue