Merge develop

This commit is contained in:
pietervdvn 2022-01-05 18:18:13 +01:00
commit ac1b4a010c
40 changed files with 5706 additions and 4746 deletions

View file

@ -16,6 +16,7 @@
+ [left_right_style](#left_right_style) + [left_right_style](#left_right_style)
+ [split_point](#split_point) + [split_point](#split_point)
+ [current_view](#current_view) + [current_view](#current_view)
+ [matchpoint](#matchpoint)
1. [Normal layers](#normal-layers) 1. [Normal layers](#normal-layers)
- [Frequently reused layers](#frequently-reused-layers) - [Frequently reused layers](#frequently-reused-layers)
+ [bicycle_library](#bicycle_library) + [bicycle_library](#bicycle_library)
@ -127,6 +128,7 @@
- [left_right_style](#left_right_style) - [left_right_style](#left_right_style)
- [split_point](#split_point) - [split_point](#split_point)
- [current_view](#current_view) - [current_view](#current_view)
- [matchpoint](#matchpoint)
### gps_location ### gps_location
@ -254,6 +256,19 @@ The icon on the button is the default icon of the layer, but can be customized b
- This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.
### matchpoint
The default rendering for a locationInput which snaps onto another object
[Go to the source code](../assets/layers/matchpoint/matchpoint.json)
- This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data. - This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.

View file

@ -18,6 +18,7 @@
+ [_now:date, _now:datetime, _loaded:date, _loaded:_datetime](#_nowdate,-_now:datetime,-_loaded:date,-_loaded:_datetime) + [_now:date, _now:datetime, _loaded:date, _loaded:_datetime](#_nowdate,-_now:datetime,-_loaded:date,-_loaded:_datetime)
+ [_last_edit:contributor, _last_edit:contributor:uid, _last_edit:changeset, _last_edit:timestamp, _version_number, _backend](#_last_editcontributor,-_last_edit:contributor:uid,-_last_edit:changeset,-_last_edit:timestamp,-_version_number,-_backend) + [_last_edit:contributor, _last_edit:contributor:uid, _last_edit:changeset, _last_edit:timestamp, _version_number, _backend](#_last_editcontributor,-_last_edit:contributor:uid,-_last_edit:changeset,-_last_edit:timestamp,-_version_number,-_backend)
+ [sidewalk:left, sidewalk:right, generic_key:left:property, generic_key:right:property](#sidewalkleft,-sidewalk:right,-generic_key:left:property,-generic_key:right:property) + [sidewalk:left, sidewalk:right, generic_key:left:property, generic_key:right:property](#sidewalkleft,-sidewalk:right,-generic_key:left:property,-generic_key:right:property)
+ [_geometry:type](#_geometrytype)
+ [distanceTo](#distanceto) + [distanceTo](#distanceto)
+ [overlapWith](#overlapwith) + [overlapWith](#overlapwith)
+ [intersectionsWith](#intersectionswith) + [intersectionsWith](#intersectionswith)
@ -151,6 +152,16 @@ Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' an
### _geometry:type
Adds the geometry type as property. This is identical to the GoeJson geometry type and is one of `Point`,`LineString`, `Polygon` and exceptionally `MultiPolygon` or `MultiLineString`
Calculating tags with Javascript Calculating tags with Javascript
---------------------------------- ----------------------------------

View file

@ -42,6 +42,10 @@
* [Example usage of tag_apply](#example-usage-of-tag_apply) * [Example usage of tag_apply](#example-usage-of-tag_apply)
+ [export_as_gpx](#export_as_gpx) + [export_as_gpx](#export_as_gpx)
* [Example usage of export_as_gpx](#example-usage-of-export_as_gpx) * [Example usage of export_as_gpx](#example-usage-of-export_as_gpx)
+ [export_as_geojson](#export_as_geojson)
* [Example usage of export_as_geojson](#example-usage-of-export_as_geojson)
+ [open_in_iD](#open_in_id)
* [Example usage of open_in_iD](#example-usage-of-open_in_id)
+ [clear_location_history](#clear_location_history) + [clear_location_history](#clear_location_history)
* [Example usage of clear_location_history](#example-usage-of-clear_location_history) * [Example usage of clear_location_history](#example-usage-of-clear_location_history)
+ [auto_apply](#auto_apply) + [auto_apply](#auto_apply)
@ -450,6 +454,22 @@ id_of_object_to_apply_this_one | _undefined_ | If specified, applies the the tag
`{export_as_gpx()}` `{export_as_gpx()}`
### export_as_geojson
Exports the selected feature as GeoJson-file
#### Example usage of export_as_geojson
`{export_as_geojson()}`
### open_in_iD
Opens the current view in the iD-editor
#### Example usage of open_in_iD
`{open_in_iD()}`
### clear_location_history ### clear_location_history
A button to remove the travelled track information from the device A button to remove the travelled track information from the device

View file

@ -85,7 +85,6 @@ class IntersectionFunc implements ExtraFunction {
const bbox = BBox.get(feat) const bbox = BBox.get(feat)
for (const layerId of layerIds) { for (const layerId of layerIds) {
console.log("Calculating the intersection with layer ", layerId)
const otherLayers = params.getFeaturesWithin(layerId, bbox) const otherLayers = params.getFeaturesWithin(layerId, bbox)
if (otherLayers === undefined) { if (otherLayers === undefined) {
continue; continue;

View file

@ -60,6 +60,12 @@ export default class FeaturePipeline {
private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>() private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>()
private readonly metataggingRecalculated = new UIEventSource<void>(undefined) private readonly metataggingRecalculated = new UIEventSource<void>(undefined)
/**
* Keeps track of all raw OSM-nodes.
* Only initialized if 'type_node' is defined as layer
*/
public readonly fullNodeDatabase? : FullNodeDatabaseSource
constructor( constructor(
handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void,
state: MapState) { state: MapState) {
@ -129,7 +135,14 @@ export default class FeaturePipeline {
this.freshnesses.set(id, new TileFreshnessCalculator()) this.freshnesses.set(id, new TileFreshnessCalculator())
if (id === "type_node") { if (id === "type_node") {
// Handles by the 'FullNodeDatabaseSource'
this.fullNodeDatabase = new FullNodeDatabaseSource(
filteredLayer,
tile => {
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
});
continue; continue;
} }
@ -248,17 +261,8 @@ export default class FeaturePipeline {
}) })
}) })
if(this.fullNodeDatabase !== undefined){
if (state.layoutToUse.trackAllNodes) { osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => this.fullNodeDatabase.handleOsmJson(osmJson, tileId))
const fullNodeDb = new FullNodeDatabaseSource(
state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0],
tile => {
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
})
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => fullNodeDb.handleOsmJson(osmJson, tileId))
} }

View file

@ -9,6 +9,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
public readonly loadedTiles = new Map<number, FeatureSource & Tiled>() public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void; private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void;
private readonly layer: FilteredLayer private readonly layer: FilteredLayer
private readonly nodeByIds = new Map<number, OsmNode>();
constructor( constructor(
layer: FilteredLayer, layer: FilteredLayer,
@ -31,6 +32,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
} }
const osmNode = <OsmNode>osmObj; const osmNode = <OsmNode>osmObj;
nodesById.set(osmNode.id, osmNode) nodesById.set(osmNode.id, osmNode)
this.nodeByIds.set(osmNode.id, osmNode)
} }
const parentWaysByNodeId = new Map<number, OsmWay[]>() const parentWaysByNodeId = new Map<number, OsmWay[]>()
@ -49,6 +51,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
} }
parentWaysByNodeId.forEach((allWays, nodeId) => { parentWaysByNodeId.forEach((allWays, nodeId) => {
nodesById.get(nodeId).tags["parent_ways"] = JSON.stringify(allWays.map(w => w.tags)) nodesById.get(nodeId).tags["parent_ways"] = JSON.stringify(allWays.map(w => w.tags))
nodesById.get(nodeId).tags["parent_way_ids"] = JSON.stringify(allWays.map(w => w.id))
}) })
const now = new Date() const now = new Date()
const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({ const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({
@ -62,6 +65,16 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
} }
/**
* Returns the OsmNode with the corresponding id (undefined if not found)
* Note that this OsmNode will have a calculated tag 'parent_ways' and 'parent_way_ids', which are resp. stringified lists of parent way tags and ids
* @param id
* @constructor
*/
public GetNode(id: number) : OsmNode {
return this.nodeByIds.get(id)
}
} }

View file

@ -357,7 +357,6 @@ export class GeoOperations {
* Returns null if the features are not intersecting * Returns null if the features are not intersecting
*/ */
private static calculateInstersection(feature, otherFeature, featureBBox: BBox, otherFeatureBBox?: BBox): number { private static calculateInstersection(feature, otherFeature, featureBBox: BBox, otherFeatureBBox?: BBox): number {
try {
if (feature.geometry.type === "LineString") { if (feature.geometry.type === "LineString") {
@ -427,19 +426,25 @@ export class GeoOperations {
return this.calculateInstersection(otherFeature, feature, otherFeatureBBox, featureBBox) return this.calculateInstersection(otherFeature, feature, otherFeatureBBox, featureBBox)
} }
try{
const intersection = turf.intersect(feature, otherFeature); const intersection = turf.intersect(feature, otherFeature);
if (intersection == null) { if (intersection == null) {
return null; return null;
} }
return turf.area(intersection); // in m² return turf.area(intersection); // in m²
}catch(e){
if(e.message === "Each LinearRing of a Polygon must have 4 or more Positions."){
// WORKAROUND TIME!
// See https://github.com/Turfjs/turf/pull/2238
return null;
}
throw e;
} }
} catch (exception) {
console.warn("EXCEPTION CAUGHT WHILE INTERSECTING: ", exception,"\nThe considered objects are",feature, otherFeature);
return undefined
} }
return undefined; throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
} }
/** /**

View file

@ -1,4 +1,4 @@
import OsmChangeAction, {OsmCreateAction} from "./OsmChangeAction"; import {OsmCreateAction} from "./OsmChangeAction";
import {Tag} from "../../Tags/Tag"; import {Tag} from "../../Tags/Tag";
import {Changes} from "../Changes"; import {Changes} from "../Changes";
import {ChangeDescription} from "./ChangeDescription"; import {ChangeDescription} from "./ChangeDescription";
@ -18,10 +18,34 @@ export interface MergePointConfig {
mode: "reuse_osm_point" | "move_osm_point" mode: "reuse_osm_point" | "move_osm_point"
} }
/**
* CreateWayWithPointreuse will create a 'CoordinateInfo' for _every_ point in the way to be created.
*
* The CoordinateInfo indicates the action to take, e.g.:
*
* - Create a new point
* - Reuse an existing OSM point (and don't move it)
* - Reuse an existing OSM point (and leave it where it is)
* - Reuse another Coordinate info (and don't do anything else with it)
*
*/
interface CoordinateInfo { interface CoordinateInfo {
/**
* The new coordinate
*/
lngLat: [number, number], lngLat: [number, number],
/**
* If set: indicates that this point is identical to an earlier point in the way and that that point should be used.
* This is especially needed in closed ways, where the last CoordinateInfo will have '0' as identicalTo
*/
identicalTo?: number, identicalTo?: number,
/**
* Information about the closebyNode which might be reused
*/
closebyNodes?: { closebyNodes?: {
/**
* Distance in meters between the target coordinate and this candidate coordinate
*/
d: number, d: number,
node: any, node: any,
config: MergePointConfig config: MergePointConfig
@ -53,6 +77,8 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
this._tags = tags; this._tags = tags;
this._state = state; this._state = state;
this._config = config; this._config = config;
// The main logic of this class: the coordinateInfo contains all the changes
this._coordinateInfo = this.CalculateClosebyNodes(coordinates); this._coordinateInfo = this.CalculateClosebyNodes(coordinates);
} }
@ -219,6 +245,9 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
return allChanges return allChanges
} }
/**
* Calculates the main changes.
*/
private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] { private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] {
const bbox = new BBox(coordinates) const bbox = new BBox(coordinates)
@ -226,6 +255,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
const allNodes = [].concat(...state.featurePipeline.GetFeaturesWithin("type_node", bbox.pad(1.2))) const allNodes = [].concat(...state.featurePipeline.GetFeaturesWithin("type_node", bbox.pad(1.2)))
const maxDistance = Math.max(...this._config.map(c => c.withinRangeOfM)) const maxDistance = Math.max(...this._config.map(c => c.withinRangeOfM))
// Init coordianteinfo with undefined but the same length as coordinates
const coordinateInfo: { const coordinateInfo: {
lngLat: [number, number], lngLat: [number, number],
identicalTo?: number, identicalTo?: number,
@ -236,6 +266,8 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
}[] }[]
}[] = coordinates.map(_ => undefined) }[] = coordinates.map(_ => undefined)
// First loop: gather all information...
for (let i = 0; i < coordinates.length; i++) { for (let i = 0; i < coordinates.length; i++) {
if (coordinateInfo[i] !== undefined) { if (coordinateInfo[i] !== undefined) {
@ -243,8 +275,11 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
continue continue
} }
const coor = coordinates[i] const coor = coordinates[i]
// Check closeby (and probably identical) point further in the coordinate list, mark them as duplicate // Check closeby (and probably identical) points further in the coordinate list, mark them as duplicate
for (let j = i + 1; j < coordinates.length; j++) { for (let j = i + 1; j < coordinates.length; j++) {
// We look into the 'future' of the way and mark those 'future' locations as being the same as this location
// The continue just above will make sure they get ignored
// This code is important to 'close' ways
if (GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) { if (GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) {
coordinateInfo[j] = { coordinateInfo[j] = {
lngLat: coor, lngLat: coor,
@ -280,6 +315,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
} }
} }
// Sort by distance, closest first
closebyNodes.sort((n0, n1) => { closebyNodes.sort((n0, n1) => {
return n0.d - n1.d return n0.d - n1.d
}) })
@ -292,8 +328,9 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
} }
let conflictFree = true;
// Second loop: figure out which point moves where without creating conflicts
let conflictFree = true;
do { do {
conflictFree = true; conflictFree = true;
for (let i = 0; i < coordinateInfo.length; i++) { for (let i = 0; i < coordinateInfo.length; i++) {

View file

@ -37,7 +37,7 @@ export default class DeleteAction extends OsmChangeAction {
} }
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
const osmObject = await OsmObject.DownloadObjectAsync(this._id) const osmObject = await OsmObject.DownloadObjectAsync(this._id)

View file

@ -11,28 +11,35 @@ import ChangeTagAction from "./ChangeTagAction";
import {And} from "../../Tags/And"; import {And} from "../../Tags/And";
import {Utils} from "../../../Utils"; import {Utils} from "../../../Utils";
import {OsmConnection} from "../OsmConnection"; import {OsmConnection} from "../OsmConnection";
import {GeoJSONObject} from "@turf/turf";
import FeaturePipeline from "../../FeatureSource/FeaturePipeline";
export default class ReplaceGeometryAction extends OsmChangeAction { export default class ReplaceGeometryAction extends OsmChangeAction {
/**
* The target feature - mostly used for the metadata
*/
private readonly feature: any; private readonly feature: any;
private readonly state: { private readonly state: {
osmConnection: OsmConnection osmConnection: OsmConnection,
featurePipeline: FeaturePipeline
}; };
private readonly wayToReplaceId: string; private readonly wayToReplaceId: string;
private readonly theme: string; private readonly theme: string;
/** /**
* The target coordinates that should end up in OpenStreetMap * The target coordinates that should end up in OpenStreetMap.
* This is identical to either this.feature.geometry.coordinates or -in case of a polygon- feature.geometry.coordinates[0]
*/ */
private readonly targetCoordinates: [number, number][]; private readonly targetCoordinates: [number, number][];
/** /**
* If a target coordinate is close to another target coordinate, 'identicalTo' will point to the first index. * If a target coordinate is close to another target coordinate, 'identicalTo' will point to the first index.
* @private
*/ */
private readonly identicalTo: number[] private readonly identicalTo: number[]
private readonly newTags: Tag[] | undefined; private readonly newTags: Tag[] | undefined;
constructor( constructor(
state: { state: {
osmConnection: OsmConnection osmConnection: OsmConnection,
featurePipeline: FeaturePipeline
}, },
feature: any, feature: any,
wayToReplaceId: string, wayToReplaceId: string,
@ -54,6 +61,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
} else if (geom.type === "Polygon") { } else if (geom.type === "Polygon") {
coordinates = geom.coordinates[0] coordinates = geom.coordinates[0]
} }
this.targetCoordinates = coordinates
this.identicalTo = coordinates.map(_ => undefined) this.identicalTo = coordinates.map(_ => undefined)
@ -68,21 +76,18 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
} }
} }
} }
this.targetCoordinates = coordinates
this.newTags = options.newTags this.newTags = options.newTags
} }
// noinspection JSUnusedGlobalSymbols
public async getPreview(): Promise<FeatureSource> { public async getPreview(): Promise<FeatureSource> {
const {closestIds, allNodesById} = await this.GetClosestIds(); const {closestIds, allNodesById, detachedNodeIds} = await this.GetClosestIds();
console.debug("Generating preview, identicals are ",) console.debug("Generating preview, identicals are ",)
const preview = closestIds.map((newId, i) => { const preview: GeoJSONObject[] = closestIds.map((newId, i) => {
if (this.identicalTo[i] !== undefined) { if (this.identicalTo[i] !== undefined) {
return undefined return undefined
} }
if (newId === undefined) { if (newId === undefined) {
return { return {
type: "Feature", type: "Feature",
@ -110,6 +115,24 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
} }
}; };
}) })
for (const detachedNodeId of detachedNodeIds) {
const origPoint = allNodesById.get(detachedNodeId).centerpoint()
const feature = {
type: "Feature",
properties: {
"detach": "yes",
"id": "replace-geometry-detach-" + detachedNodeId
},
geometry: {
type: "Point",
coordinates: [origPoint[1], origPoint[0]]
}
};
preview.push(feature)
}
return new StaticFeatureSource(Utils.NoNull(preview), false) return new StaticFeatureSource(Utils.NoNull(preview), false)
} }
@ -119,7 +142,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
const allChanges: ChangeDescription[] = [] const allChanges: ChangeDescription[] = []
const actualIdsToUse: number[] = [] const actualIdsToUse: number[] = []
const {closestIds, osmWay} = await this.GetClosestIds() const {closestIds, osmWay, detachedNodeIds} = await this.GetClosestIds()
for (let i = 0; i < closestIds.length; i++) { for (let i = 0; i < closestIds.length; i++) {
if (this.identicalTo[i] !== undefined) { if (this.identicalTo[i] !== undefined) {
@ -170,7 +193,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes)) allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes))
} }
// AT the very last: actually change the nodes of the way! // Actually change the nodes of the way!
allChanges.push({ allChanges.push({
type: "way", type: "way",
id: osmWay.id, id: osmWay.id,
@ -185,18 +208,67 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
}) })
// Some nodes might need to be deleted
if (detachedNodeIds.length > 0) {
const nodeDb = this.state.featurePipeline.fullNodeDatabase;
if (nodeDb === undefined) {
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
}
for (const nodeId of detachedNodeIds) {
const osmNode = nodeDb.GetNode(nodeId)
const parentWayIds: number[] = JSON.parse(osmNode.tags["parent_way_ids"])
const index = parentWayIds.indexOf(osmWay.id)
if(index < 0){
console.error("ReplaceGeometryAction is trying to detach node "+nodeId+", but it isn't listed as being part of way "+osmWay.id)
continue;
}
parentWayIds.splice(index, 1)
osmNode.tags["parent_way_ids"] = JSON.stringify(parentWayIds)
if(parentWayIds.length == 0){
// This point has no other ways anymore - lets clean it!
console.log("Removing node "+nodeId, "as it isn't needed anymore by any way")
allChanges.push({
meta: {
theme: this.theme,
changeType: "delete"
},
doDelete: true,
type: "node",
id: nodeId,
})
}
}
}
return allChanges return allChanges
} }
/** /**
* For 'this.feature`, gets a corresponding closest node that alreay exsists * For 'this.feature`, gets a corresponding closest node that alreay exsists.
* @constructor *
* @private * This method contains the main logic for this module, as it decides which node gets moved where.
*
*/ */
private async GetClosestIds(): Promise<{ closestIds: number[], allNodesById: Map<number, OsmNode>, osmWay: OsmWay }> { private async GetClosestIds(): Promise<{
// A list of the same length as targetCoordinates, containing which OSM-point to move. If undefined, a new point will be created
closestIds: number[],
allNodesById: Map<number, OsmNode>,
osmWay: OsmWay,
detachedNodeIds: number[]
}> {
// TODO FIXME: cap move length on points which are embedded into other ways (ev. disconnect them) // TODO FIXME: cap move length on points which are embedded into other ways (ev. disconnect them)
// TODO FIXME: if a new point has to be created, snap to already existing ways // TODO FIXME: if a new point has to be created, snap to already existing ways
// TODO FIXME: detect intersections with other ways if moved
let parsed: OsmObject[];
{
// Gather the needed OsmObjects
const splitted = this.wayToReplaceId.split("/"); const splitted = this.wayToReplaceId.split("/");
const type = splitted[0]; const type = splitted[0];
const idN = Number(splitted[1]); const idN = Number(splitted[1]);
@ -205,72 +277,101 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
} }
const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`; const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`;
const rawData = await Utils.downloadJsonCached(url, 1000) const rawData = await Utils.downloadJsonCached(url, 1000)
const parsed = OsmObject.ParseObjects(rawData.elements); parsed = OsmObject.ParseObjects(rawData.elements);
const allNodesById = new Map<number, OsmNode>()
const allNodes = parsed.filter(o => o.type === "node")
for (const node of allNodes) {
allNodesById.set(node.id, <OsmNode>node)
} }
const allNodes = parsed.filter(o => o.type === "node")
/** /**
* Allright! We know all the nodes of the original way and all the nodes of the target coordinates. * For every already existing OSM-point, we calculate the distance to every target point
* For each of the target coordinates, we search the closest, already existing point and reuse this point
*/ */
const closestIds = [] const distances = new Map<number /* osmId*/, number[] /* target coordinate index --> distance (or undefined if a duplicate)*/>();
const distances = [] for (const node of allNodes) {
const nodeDistances = this.targetCoordinates.map(_ => undefined)
for (let i = 0; i < this.targetCoordinates.length; i++) { for (let i = 0; i < this.targetCoordinates.length; i++) {
const target = this.targetCoordinates[i];
let closestDistance = undefined
let closestId = undefined;
for (const osmNode of allNodes) {
const cp = osmNode.centerpoint()
const d = GeoOperations.distanceBetween(target, [cp[1], cp[0]])
if (closestId === undefined || closestDistance > d) {
closestId = osmNode.id
closestDistance = d
}
}
closestIds.push(closestId)
distances.push(closestDistance)
}
// Next step: every closestId can only occur once in the list
// We skip the ones which are identical
console.log("Erasing double ids")
for (let i = 0; i < closestIds.length; i++) {
if (this.identicalTo[i] !== undefined) { if (this.identicalTo[i] !== undefined) {
closestIds[i] = closestIds[this.identicalTo[i]] continue;
continue
} }
const closestId = closestIds[i] const targetCoordinate = this.targetCoordinates[i];
for (let j = i + 1; j < closestIds.length; j++) { const cp = node.centerpoint()
if (this.identicalTo[j] !== undefined) { nodeDistances[i] = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]])
continue
} }
const otherClosestId = closestIds[j] distances.set(node.id, nodeDistances)
if (closestId !== otherClosestId) {
continue
} }
// We have two occurences of 'closestId' - we only keep the closest instance!
const di = distances[i] /**
const dj = distances[j] * Then, we search the node that has to move the least distance and add this as mapping.
if (di < dj) { * We do this until no points are left
closestIds[j] = undefined */
let candidate: number;
let moveDistance: number;
const closestIds = this.targetCoordinates.map(_ => undefined)
/**
* The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates
*/
const unusedIds = []
do {
candidate = undefined;
moveDistance = Infinity;
distances.forEach((distances, nodeId) => {
const minDist = Math.min(...Utils.NoNull(distances))
if (moveDistance > minDist) {
// We have found a candidate to move
candidate = nodeId
moveDistance = minDist
}
})
if (candidate !== undefined) {
// We found a candidate... Search the corresponding target id:
let targetId: number = undefined;
let lowestDistance = Number.MAX_VALUE
let nodeDistances = distances.get(candidate)
for (let i = 0; i < nodeDistances.length; i++) {
const d = nodeDistances[i]
if (d !== undefined && d < lowestDistance) {
lowestDistance = d;
targetId = i;
}
}
// This candidates role is done, it can be removed from the distance matrix
distances.delete(candidate)
if (targetId !== undefined) {
// At this point, we have our target coordinate index: targetId!
// Lets map it...
closestIds[targetId] = candidate
// To indicate that this targetCoordinate is taken, we remove them from the distances matrix
distances.forEach(dists => {
dists[targetId] = undefined
})
} else { } else {
closestIds[i] = undefined // Seems like all the targetCoordinates have found a source point
} unusedIds.push(candidate)
} }
} }
} while (candidate !== undefined)
// If there are still unused values in 'distances', they are definitively unused
distances.forEach((_, nodeId) => {
unusedIds.push(nodeId)
})
{
// Some extra data is included for rendering
const osmWay = <OsmWay>parsed[parsed.length - 1] const osmWay = <OsmWay>parsed[parsed.length - 1]
if (osmWay.type !== "way") { if (osmWay.type !== "way") {
throw "WEIRD: expected an OSM-way as last element here!" throw "WEIRD: expected an OSM-way as last element here!"
} }
return {closestIds, allNodesById, osmWay}; const allNodesById = new Map<number, OsmNode>()
for (const node of allNodes) {
allNodesById.set(node.id, <OsmNode>node)
}
return {closestIds, allNodesById, osmWay, detachedNodeIds: unusedIds};
}
} }

View file

@ -384,8 +384,8 @@ export class Changes {
states.set(o.type + "/" + o.id, "unchanged") states.set(o.type + "/" + o.id, "unchanged")
} }
let changed = false;
for (const change of changes) { for (const change of changes) {
let changed = false;
const id = change.type + "/" + change.id const id = change.type + "/" + change.id
if (!objects.has(id)) { if (!objects.has(id)) {
// The object hasn't been seen before, so it doesn't exist yet and is newly created by its very definition // The object hasn't been seen before, so it doesn't exist yet and is newly created by its very definition
@ -493,7 +493,7 @@ export class Changes {
} }
if (changed && state === "unchanged") { if (changed && states.get(id) === "unchanged") {
states.set(id, "modified") states.set(id, "modified")
} }
} }
@ -520,6 +520,7 @@ export class Changes {
}) })
console.debug("Calculated the pending changes: ", result.newObjects.length,"new; ", result.modifiedObjects.length,"modified;",result.deletedObjects,"deleted")
return result return result
} }
} }

View file

@ -7,7 +7,6 @@ import Title from "../UI/Base/Title";
import {FixedUiElement} from "../UI/Base/FixedUiElement"; import {FixedUiElement} from "../UI/Base/FixedUiElement";
import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import {CountryCoder} from "latlon2country" import {CountryCoder} from "latlon2country"
import ScriptUtils from "../scripts/ScriptUtils";
export class SimpleMetaTagger { export class SimpleMetaTagger {
@ -409,7 +408,21 @@ export default class SimpleMetaTaggers {
feature.properties["_loaded:datetime"] = datetime(freshness); feature.properties["_loaded:datetime"] = datetime(freshness);
return true; return true;
} }
);
public static geometryType = new SimpleMetaTagger(
{
keys:["_geometry:type"],
doc: "Adds the geometry type as property. This is identical to the GoeJson geometry type and is one of `Point`,`LineString`, `Polygon` and exceptionally `MultiPolygon` or `MultiLineString`",
},
(feature, _) => {
const changed = feature.properties["_geometry:type"] === feature.geometry.type;
feature.properties["_geometry:type"] = feature.geometry.type;
return changed
}
) )
public static metatags: SimpleMetaTagger[] = [ public static metatags: SimpleMetaTagger[] = [
SimpleMetaTaggers.latlon, SimpleMetaTaggers.latlon,
SimpleMetaTaggers.layerInfo, SimpleMetaTaggers.layerInfo,
@ -421,7 +434,8 @@ export default class SimpleMetaTaggers {
SimpleMetaTaggers.directionSimplified, SimpleMetaTaggers.directionSimplified,
SimpleMetaTaggers.currentTime, SimpleMetaTaggers.currentTime,
SimpleMetaTaggers.objectMetaInfo, SimpleMetaTaggers.objectMetaInfo,
SimpleMetaTaggers.noBothButLeftRight SimpleMetaTaggers.noBothButLeftRight,
SimpleMetaTaggers.geometryType
]; ];

View file

@ -59,7 +59,7 @@ export class And extends TagsFilter {
} }
asHumanString(linkToWiki: boolean, shorten: boolean, properties) { asHumanString(linkToWiki: boolean, shorten: boolean, properties) {
return this.and.map(t => t.asHumanString(linkToWiki, shorten, properties)).join("&"); return this.and.map(t => t.asHumanString(linkToWiki, shorten, properties)).filter(x => x !== "").join("&");
} }
isUsableAsAnswer(): boolean { isUsableAsAnswer(): boolean {

View file

@ -41,12 +41,17 @@ export class Tag extends TagsFilter {
return [`["${this.key}"="${this.value}"]`]; return [`["${this.key}"="${this.value}"]`];
} }
asHumanString(linkToWiki?: boolean, shorten?: boolean) { asHumanString(linkToWiki?: boolean, shorten?: boolean, currentProperties?: any) {
let v = this.value; let v = this.value;
if (shorten) { if (shorten) {
v = Utils.EllipsesAfter(v, 25); v = Utils.EllipsesAfter(v, 25);
} }
if(v === "" || v === undefined){ if(v === "" || v === undefined){
// This tag will be removed if in the properties, so we indicate this with special rendering
if(currentProperties !== undefined && (currentProperties[this.key] ?? "") === ""){
// This tag is not present in the current properties, so this tag doesn't change anything
return ""
}
return "<span class='line-through'>"+this.key+"</span>" return "<span class='line-through'>"+this.key+"</span>"
} }
if (linkToWiki) { if (linkToWiki) {

View file

@ -281,10 +281,12 @@ export class UIEventSource<T> {
* @param f: The transforming function * @param f: The transforming function
* @param extraSources: also trigger the update if one of these sources change * @param extraSources: also trigger the update if one of these sources change
* @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData * @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData
* @param allowUnregister: if set, the update will be halted if no listeners are registered
*/ */
public map<J>(f: ((t: T) => J), public map<J>(f: ((t: T) => J),
extraSources: UIEventSource<any>[] = [], extraSources: UIEventSource<any>[] = [],
g: ((j: J, t: T) => T) = undefined): UIEventSource<J> { g: ((j: J, t: T) => T) = undefined,
allowUnregister = false): UIEventSource<J> {
const self = this; const self = this;
const stack = new Error().stack.split("\n"); const stack = new Error().stack.split("\n");
@ -297,6 +299,7 @@ export class UIEventSource<T> {
const update = function () { const update = function () {
newSource.setData(f(self.data)); newSource.setData(f(self.data));
return allowUnregister && newSource._callbacks.length === 0
} }
this.addCallback(update); this.addCallback(update);

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants { export default class Constants {
public static vNumber = "0.13.0-alpha-9"; public static vNumber = "0.14.0-alpha-1";
public static ImgurApiKey = '7070e7167f0a25a' public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"

View file

@ -73,7 +73,13 @@ export default class LayerConfig extends WithContextLoader {
if (json.source.osmTags === undefined) { if (json.source.osmTags === undefined) {
throw "Layer " + this.id + " does not define a osmTags in the source section - these should always be present, even for geojson layers (" + context + ")" throw "Layer " + this.id + " does not define a osmTags in the source section - these should always be present, even for geojson layers (" + context + ")"
}
if(json.id.toLowerCase() !== json.id){
throw `${context}: The id of a layer should be lowercase: ${json.id}`
}
if(json.id.match(/[a-z0-9-_]/) == null){
throw `${context}: The id of a layer should match [a-z0-9-_]*: ${json.id}`
} }
this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30 this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30

View file

@ -50,17 +50,21 @@ export default class LayoutConfig {
public readonly overpassMaxZoom: number public readonly overpassMaxZoom: number
public readonly osmApiTileSize: number public readonly osmApiTileSize: number
public readonly official: boolean; public readonly official: boolean;
public readonly trackAllNodes: boolean;
constructor(json: LayoutConfigJson, official = true, context?: string) { constructor(json: LayoutConfigJson, official = true, context?: string) {
this.official = official; this.official = official;
this.id = json.id; this.id = json.id;
if(json.id.toLowerCase() !== json.id){
throw "The id of a theme should be lowercase: "+json.id
}
if(json.id.match(/[a-z0-9-_]/) == null){
throw "The id of a theme should match [a-z0-9-_]*: "+json.id
}
context = (context ?? "") + "." + this.id; context = (context ?? "") + "." + this.id;
this.maintainer = json.maintainer; this.maintainer = json.maintainer;
this.credits = json.credits; this.credits = json.credits;
this.version = json.version; this.version = json.version;
this.language = []; this.language = [];
this.trackAllNodes = false
if (typeof json.language === "string") { if (typeof json.language === "string") {
this.language = [json.language]; this.language = [json.language];
@ -105,7 +109,6 @@ export default class LayoutConfig {
this.tileLayerSources = (json.tileLayerSources ?? []).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`)) this.tileLayerSources = (json.tileLayerSources ?? []).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`))
// At this point, layers should be expanded and validated either by the generateScript or the LegacyJsonConvert // At this point, layers should be expanded and validated either by the generateScript or the LegacyJsonConvert
this.layers = json.layers.map(lyrJson => new LayerConfig(<LayerConfigJson>lyrJson, json.id + ".layers." + lyrJson["id"], official)); this.layers = json.layers.map(lyrJson => new LayerConfig(<LayerConfigJson>lyrJson, json.id + ".layers." + lyrJson["id"], official));
this.trackAllNodes = this.layers.some(layer => layer.id === "type_node");
this.clustering = { this.clustering = {

View file

@ -19,9 +19,82 @@ import Loc from "../../Models/Loc";
import Toggle from "../Input/Toggle"; import Toggle from "../Input/Toggle";
import {OsmConnection} from "../../Logic/Osm/OsmConnection"; import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import PrivacyPolicy from "./PrivacyPolicy";
import ContributorCount from "../../Logic/ContributorCount"; import ContributorCount from "../../Logic/ContributorCount";
export class OpenIdEditor extends VariableUiElement {
constructor(state : {locationControl: UIEventSource<Loc>}, iconStyle? : string, objectId?: string) {
const t = Translations.t.general.attribution
super(state.locationControl.map(location => {
let elementSelect = "";
if(objectId !== undefined){
const parts = objectId.split("/")
const tp = parts[0]
if(parts.length === 2 && !isNaN(Number(parts[1])) && (tp === "node" || tp === "way" || tp === "relation")){
elementSelect = "&"+ tp+"="+parts[1]
}
}
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}`
return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {url: idLink, newTab: true})
}));
}
}
export class OpenMapillary extends VariableUiElement {
constructor(state : {locationControl: UIEventSource<Loc>}, iconStyle? : string) {
const t = Translations.t.general.attribution
super( state.locationControl.map(location => {
const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), t.openMapillary, {
url: mapillaryLink,
newTab: true
})
}))
}
}
export class OpenJosm extends Combine {
constructor(state : {osmConnection: OsmConnection, currentBounds: UIEventSource<BBox>,}, iconStyle? : string) {
const t = Translations.t.general.attribution
const josmState = new UIEventSource<string>(undefined)
// Reset after 15s
josmState.stabilized(15000).addCallbackD(_ => josmState.setData(undefined))
const stateIndication = new VariableUiElement(josmState.map(state => {
if (state === undefined) {
return undefined
}
state = state.toUpperCase()
if (state === "OK") {
return t.josmOpened.SetClass("thanks")
}
return t.josmNotOpened.SetClass("alert")
}));
const toggle = new Toggle(
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => {
const bounds: any = state.currentBounds.data;
if (bounds === undefined) {
return undefined
}
const top = bounds.getNorth();
const bottom = bounds.getSouth();
const right = bounds.getEast();
const left = bounds.getWest();
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
Utils.download(josmLink).then(answer => josmState.setData(answer.replace(/\n/g, '').trim())).catch(_ => josmState.setData("ERROR"))
}), undefined, state.osmConnection.userDetails.map(ud => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible))
super([stateIndication, toggle]);
}
}
/** /**
* The attribution panel shown on mobile * The attribution panel shown on mobile
*/ */
@ -39,9 +112,6 @@ export default class CopyrightPanel extends Combine {
const t = Translations.t.general.attribution const t = Translations.t.general.attribution
const layoutToUse = state.layoutToUse const layoutToUse = state.layoutToUse
const josmState = new UIEventSource<string>(undefined)
// Reset after 15s
josmState.stabilized(15000).addCallbackD(_ => josmState.setData(undefined))
const iconStyle = "height: 1.5rem; width: auto" const iconStyle = "height: 1.5rem; width: auto"
const actionButtons = [ const actionButtons = [
new SubtleButton(Svg.liberapay_ui().SetStyle(iconStyle), t.donate, { new SubtleButton(Svg.liberapay_ui().SetStyle(iconStyle), t.donate, {
@ -56,42 +126,9 @@ export default class CopyrightPanel extends Combine {
url: Utils.OsmChaLinkFor(31, state.layoutToUse.id), url: Utils.OsmChaLinkFor(31, state.layoutToUse.id),
newTab: true newTab: true
}), }),
new VariableUiElement(state.locationControl.map(location => { new OpenIdEditor(state, iconStyle),
const idLink = `https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}` new OpenMapillary(state, iconStyle),
return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {url: idLink, newTab: true}) new OpenJosm(state, iconStyle)
})),
new VariableUiElement(state.locationControl.map(location => {
const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), t.openMapillary, {
url: mapillaryLink,
newTab: true
})
})),
new VariableUiElement(josmState.map(state => {
if (state === undefined) {
return undefined
}
state = state.toUpperCase()
if (state === "OK") {
return t.josmOpened.SetClass("thanks")
}
return t.josmNotOpened.SetClass("alert")
})),
new Toggle(
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => {
const bounds: any = state.currentBounds.data;
if (bounds === undefined) {
return undefined
}
const top = bounds.getNorth();
const bottom = bounds.getSouth();
const right = bounds.getEast();
const left = bounds.getWest();
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
Utils.download(josmLink).then(answer => josmState.setData(answer.replace(/\n/g, '').trim())).catch(_ => josmState.setData("ERROR"))
}), undefined, state.osmConnection.userDetails.map(ud => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible)),
] ]
const iconAttributions = Utils.NoNull(Array.from(layoutToUse.ExtractImages())) const iconAttributions = Utils.NoNull(Array.from(layoutToUse.ExtractImages()))

View file

@ -350,8 +350,19 @@ export default class ValidatedTextField {
ValidatedTextField.tp( ValidatedTextField.tp(
"email", "email",
"An email adress", "An email adress",
(str) => EmailValidator.validate(str), (str) => {
undefined, if(str.startsWith("mailto:")){
str = str.substring("mailto:".length)
}
return EmailValidator.validate(str);
},
str => {
if(str === undefined){return undefined}
if(str.startsWith("mailto:")){
str = str.substring("mailto:".length)
}
return str;
},
undefined, undefined,
"email"), "email"),
ValidatedTextField.tp( ValidatedTextField.tp(
@ -395,9 +406,17 @@ export default class ValidatedTextField {
if (str === undefined) { if (str === undefined) {
return false; return false;
} }
if(str.startsWith("tel:")){
str = str.substring("tel:".length)
}
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.isValid() ?? false return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.isValid() ?? false
}, },
(str, country: () => string) => parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational(), (str, country: () => string) => {
if(str.startsWith("tel:")){
str = str.substring("tel:".length)
}
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational();
},
undefined, undefined,
"tel" "tel"
), ),

View file

@ -189,8 +189,13 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
new VariableUiElement( new VariableUiElement(
State.state.featureSwitchIsDebugging.map(isDebugging => { State.state.featureSwitchIsDebugging.map(isDebugging => {
if (isDebugging) { if (isDebugging) {
const config: TagRenderingConfig = new TagRenderingConfig({render: "{all_tags()}"}, ""); const config_all_tags: TagRenderingConfig = new TagRenderingConfig({render: "{all_tags()}"}, "");
return new TagRenderingAnswer(tags, config, "all_tags") const config_download: TagRenderingConfig = new TagRenderingConfig({render: "{export_as_geojson()}"}, "");
const config_id: TagRenderingConfig = new TagRenderingConfig({render: "{open_in_iD()}"}, "");
return new Combine([new TagRenderingAnswer(tags, config_all_tags, "all_tags"),
new TagRenderingAnswer(tags, config_download, ""),
new TagRenderingAnswer(tags, config_id, "")])
} }
}) })
) )

View file

@ -233,7 +233,7 @@ ${Utils.special_visualizations_importRequirementDocs}
onCancel: () => void): BaseUIElement { onCancel: () => void): BaseUIElement {
const self = this; const self = this;
const confirmationMap = Minimap.createMiniMap({ const confirmationMap = Minimap.createMiniMap({
allowMoving: false, allowMoving: state.featureSwitchIsDebugging.data ?? false,
background: state.backgroundLayer background: state.backgroundLayer
}) })
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl") confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
@ -298,10 +298,19 @@ export class ConflateButton extends AbstractImportButton {
return feature.geometry.type === "LineString" || (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1) return feature.geometry.type === "LineString" || (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
} }
getLayerDependencies(argsRaw: string[]): string[] {
const deps = super.getLayerDependencies(argsRaw);
// Force 'type_node' as dependency
deps.push("type_node")
return deps;
}
constructElement(state: FeaturePipelineState, constructElement(state: FeaturePipelineState,
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<Tag[]>; targetLayer: string }, args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<Tag[]>; targetLayer: string },
tagSource: UIEventSource<any>, guiState: DefaultGuiState, feature: any, onCancelClicked: () => void): BaseUIElement { tagSource: UIEventSource<any>, guiState: DefaultGuiState, feature: any, onCancelClicked: () => void): BaseUIElement {
return new FixedUiElement("ReplaceGeometry is currently very broken - use mapcomplete.osm.be for now").SetClass("alert")
const nodesMustMatch = args.snap_onto_layers?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) const nodesMustMatch = args.snap_onto_layers?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
const mergeConfigs = [] const mergeConfigs = []
@ -357,18 +366,18 @@ export class ImportWayButton extends AbstractImportButton {
{ {
name: "move_osm_point_if", name: "move_osm_point_if",
doc: "Moves the OSM-point to the newly imported point if these conditions are met", doc: "Moves the OSM-point to the newly imported point if these conditions are met",
},{ }, {
name:"max_move_distance", name: "max_move_distance",
doc: "If an OSM-point is moved, the maximum amount of meters it is moved. Capped on 20m", doc: "If an OSM-point is moved, the maximum amount of meters it is moved. Capped on 20m",
defaultValue: "1" defaultValue: "1"
},{ }, {
name:"snap_onto_layers", name: "snap_onto_layers",
doc:"If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead", doc: "If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead",
},{ }, {
name:"snap_to_layer_max_distance", name: "snap_to_layer_max_distance",
doc:"Distance to distort the geometry to snap to this layer", doc: "Distance to distort the geometry to snap to this layer",
defaultValue: "0.1" defaultValue: "0.1"
}], }],
false false
) )
@ -427,7 +436,7 @@ defaultValue: "0.1"
if (nodesMustMatch !== undefined && moveOsmPointIfTags.length > 0) { if (nodesMustMatch !== undefined && moveOsmPointIfTags.length > 0) {
const moveDistance = Math.min(20, Number(args["max_move_distance"])) const moveDistance = Math.min(20, Number(args["max_move_distance"]))
const mergeConfig: MergePointConfig = { const mergeConfig: MergePointConfig = {
mode: "move_osm_point" , mode: "move_osm_point",
ifMatches: new And(moveOsmPointIfTags), ifMatches: new And(moveOsmPointIfTags),
withinRangeOfM: moveDistance withinRangeOfM: moveDistance
} }

View file

@ -146,6 +146,7 @@ export default class ShowDataLayer {
onEachFeature: (feature, leafletLayer) => self.postProcessFeature(feature, leafletLayer) onEachFeature: (feature, leafletLayer) => self.postProcessFeature(feature, leafletLayer)
}); });
const selfLayer = this.geoLayer;
const allFeats = this._features.features.data; const allFeats = this._features.features.data;
for (const feat of allFeats) { for (const feat of allFeats) {
if (feat === undefined) { if (feat === undefined) {
@ -153,12 +154,11 @@ export default class ShowDataLayer {
} }
try { try {
if (feat.geometry.type === "LineString") { if (feat.geometry.type === "LineString") {
const self = this;
const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates) const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates)
const tagsSource = this.allElements?.addOrGetElement(feat) ?? new UIEventSource<any>(feat.properties); const tagsSource = this.allElements?.addOrGetElement(feat) ?? new UIEventSource<any>(feat.properties);
let offsettedLine; let offsettedLine;
tagsSource tagsSource
.map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags)) .map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags), [], undefined, true)
.withEqualityStabilized((a, b) => { .withEqualityStabilized((a, b) => {
if (a === b) { if (a === b) {
return true return true
@ -176,6 +176,9 @@ export default class ShowDataLayer {
offsettedLine = L.polyline(coords, lineStyle); offsettedLine = L.polyline(coords, lineStyle);
this.postProcessFeature(feat, offsettedLine) this.postProcessFeature(feat, offsettedLine)
offsettedLine.addTo(this.geoLayer) offsettedLine.addTo(this.geoLayer)
// If 'self.geoLayer' is not the same as the layer the feature is added to, we can safely remove this callback
return self.geoLayer !== selfLayer
}) })
} else { } else {
this.geoLayer.addData(feat); this.geoLayer.addData(feat);
@ -186,6 +189,7 @@ export default class ShowDataLayer {
} }
if (options.zoomToFeatures ?? false) { if (options.zoomToFeatures ?? false) {
if(this.geoLayer.getLayers().length > 0){
try { try {
const bounds = this.geoLayer.getBounds() const bounds = this.geoLayer.getBounds()
mp.fitBounds(bounds, {animate: false}) mp.fitBounds(bounds, {animate: false})
@ -193,6 +197,7 @@ export default class ShowDataLayer {
console.debug("Invalid bounds", e) console.debug("Invalid bounds", e)
} }
} }
}
if (options.doShowLayer?.data ?? true) { if (options.doShowLayer?.data ?? true) {
mp.addLayer(this.geoLayer) mp.addLayer(this.geoLayer)

View file

@ -37,6 +37,7 @@ import {ConflateButton, ImportPointButton, ImportWayButton} from "./Popup/Import
import TagApplyButton from "./Popup/TagApplyButton"; import TagApplyButton from "./Popup/TagApplyButton";
import AutoApplyButton from "./Popup/AutoApplyButton"; import AutoApplyButton from "./Popup/AutoApplyButton";
import * as left_right_style_json from "../assets/layers/left_right_style/left_right_style.json"; import * as left_right_style_json from "../assets/layers/left_right_style/left_right_style.json";
import {OpenIdEditor} from "./BigComponents/CopyrightPanel";
export interface SpecialVisualization { export interface SpecialVisualization {
funcName: string, funcName: string,
@ -542,7 +543,7 @@ export default class SpecialVisualizations {
const t = Translations.t.general.download; const t = Translations.t.general.download;
return new SubtleButton(Svg.download_ui(), return new SubtleButton(Svg.download_ui(),
new Combine([t.downloadGpx.SetClass("font-bold text-lg"), new Combine([t.downloadFeatureAsGpx.SetClass("font-bold text-lg"),
t.downloadGpxHelper.SetClass("subtle")]).SetClass("flex flex-col") t.downloadGpxHelper.SetClass("subtle")]).SetClass("flex flex-col")
).onClick(() => { ).onClick(() => {
console.log("Exporting as GPX!") console.log("Exporting as GPX!")
@ -559,6 +560,41 @@ export default class SpecialVisualizations {
}) })
} }
}, },
{
funcName: "export_as_geojson",
docs: "Exports the selected feature as GeoJson-file",
args: [],
constr: (state, tagSource, args) => {
const t = Translations.t.general.download;
return new SubtleButton(Svg.download_ui(),
new Combine([t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"),
t.downloadGeoJsonHelper.SetClass("subtle")]).SetClass("flex flex-col")
).onClick(() => {
console.log("Exporting as Geojson")
const tags = tagSource.data
const feature = state.allElements.ContainingFeatures.get(tags.id)
const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags)
const title = matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson"
const data = JSON.stringify(feature, null, " ");
Utils.offerContentsAsDownloadableFile(data, title + "_mapcomplete_export.geojson", {
mimetype: "application/vnd.geo+json"
})
})
}
},
{
funcName: "open_in_iD",
docs: "Opens the current view in the iD-editor",
args: [],
constr: (state, feature ) => {
return new OpenIdEditor(state, undefined, feature.data.id)
}
},
{ {
funcName: "clear_location_history", funcName: "clear_location_history",
docs: "A button to remove the travelled track information from the device", docs: "A button to remove the travelled track information from the device",

View file

@ -138,10 +138,11 @@ export default class WikidataPreviewBox extends VariableUiElement {
const key = extraProperty.property const key = extraProperty.property
const display = extraProperty.display const display = extraProperty.display
const value: string[] = Array.from(wikidata.claims.get(key)) if (wikidata.claims?.get(key) === undefined) {
if (value === undefined) {
continue continue
} }
const value: string[] = Array.from(wikidata.claims.get(key))
if (display instanceof Translation) { if (display instanceof Translation) {
els.push(display.Subs({value: value.join(", ")}).SetClass("m-2")) els.push(display.Subs({value: value.join(", ")}).SetClass("m-2"))
continue continue

View file

@ -80,7 +80,7 @@
"de": "Fahrradhindernis" "de": "Fahrradhindernis"
}, },
"tags": [ "tags": [
"barrier=bollard" "barrier=cycle_barrier"
], ],
"description": { "description": {
"en": "Cycle barrier, slowing down cyclists", "en": "Cycle barrier, slowing down cyclists",
@ -125,6 +125,23 @@
], ],
"id": "bicycle=yes/no" "id": "bicycle=yes/no"
}, },
{
"id": "barrier_type",
"mappings": [
{
"if": "barrier=bollard",
"then": {
"en": "This is a single bollard in the road"
}
},{
"if": "barrier=cycle_barrier",
"then": {
"en": "This is a cycle barrier slowing down cyclists",
"nl": "Dit zijn fietshekjes die fietsers afremmen"
}
}
]
},
{ {
"question": { "question": {
"en": "What kind of bollard is this?", "en": "What kind of bollard is this?",

View file

@ -15,7 +15,15 @@
"mapRendering": [ "mapRendering": [
{ {
"location": "point", "location": "point",
"icon": "addSmall:#000", "icon": {
"render": "addSmall:#000",
"mappings": [
{
"if": "detach=yes",
"then": "circle:white;close:#c33"
}
]
},
"iconSize": "10,10,center" "iconSize": "10,10,center"
}, },
{ {

View file

@ -184,8 +184,15 @@
} }
] ]
}, },
"fill": "no",
"width": { "width": {
"render": "8" "render": "8",
"mappings": [
{
"if": "_geometry:type=Polygon",
"then": "16"
}
]
} }
} }
] ]

View file

@ -543,7 +543,7 @@
"condition": "cuisine=friture" "condition": "cuisine=friture"
}, },
"service:electricity", "service:electricity",
"dog-access" "dog-access","reviews"
], ],
"filter": [ "filter": [
{ {

View file

@ -71,9 +71,17 @@
"ca": "Quin és el telèfon de {name}?" "ca": "Quin és el telèfon de {name}?"
}, },
"render": "<a href='tel:{phone}'>{phone}</a>", "render": "<a href='tel:{phone}'>{phone}</a>",
"mappings": [
{
"if": "contact:phone~*",
"then": "<a href='tel:{contact:phone}'>{contact:phone}</a>",
"hideInAnswer": true
}
],
"freeform": { "freeform": {
"key": "phone", "key": "phone",
"type": "phone" "type": "phone",
"addExtraTags": ["contact:phone="]
} }
}, },
"osmlink": { "osmlink": {
@ -140,9 +148,17 @@
"hu": "Mi a(z) {name} e-mail címe?", "hu": "Mi a(z) {name} e-mail címe?",
"ca": "Quina és l'adreça de correu electrònic de {name}?" "ca": "Quina és l'adreça de correu electrònic de {name}?"
}, },
"mappings": [
{
"if": "contact:email~*",
"then": "<a href='mailto:{contact:email}' target='_blank'>{contact:email}</a>",
"hideInAnswer": true
}
],
"freeform": { "freeform": {
"key": "email", "key": "email",
"type": "email" "type": "email",
"addExtraTags": ["contact:email="]
} }
}, },
"website": { "website": {
@ -168,8 +184,16 @@
"render": "<a href='{website}' target='_blank'>{website}</a>", "render": "<a href='{website}' target='_blank'>{website}</a>",
"freeform": { "freeform": {
"key": "website", "key": "website",
"type": "url" "type": "url",
"addExtraTags": ["contact:website="]
},
"mappings": [
{
"if": "contact:website~*",
"then": "<a href='{contact:website}' target='_blank'>{contact:website}</a>",
"hideInAnswer": true
} }
]
}, },
"wheelchair-access": { "wheelchair-access": {
"question": { "question": {

View file

@ -18,7 +18,7 @@
"layers": [ "layers": [
"defibrillator", "defibrillator",
{ {
"id": "Brugge", "id": "brugge",
"name": "Brugse dataset", "name": "Brugse dataset",
"source": { "source": {
"osmTags": "Brugs volgnummer~*", "osmTags": "Brugs volgnummer~*",

View file

@ -93,7 +93,7 @@
} }
}, },
{ {
"id": "OSM-buildings", "id": "osm-buildings",
"name": "All OSM-buildings", "name": "All OSM-buildings",
"source": { "source": {
"osmTags": "building~*", "osmTags": "building~*",
@ -301,6 +301,24 @@
} }
] ]
}, },
{
"id": "service_ways",
"name": "Service roads",
"description": "A seperate layer with service roads, as to remove them from the intersection testing",
"source": {
"osmTags": "highway=service"
},
"mapRendering": [
{
"width": 4,
"color": "#888888"
}
],
"title": {
"render": "Service road"
},
"tagRenderings": []
},
{ {
"id": "generic_osm_object", "id": "generic_osm_object",
"name": "All OSM Objects", "name": "All OSM Objects",
@ -354,13 +372,13 @@
"builtin": "crab_address", "builtin": "crab_address",
"override": { "override": {
"calculatedTags+": [ "calculatedTags+": [
"_embedded_in=feat.overlapWith('OSM-buildings').filter(b => /* Do not match newly created objects */ b.feat.properties.id.indexOf('-') < 0)[0]?.feat?.properties ?? {}", "_embedded_in=feat.overlapWith('osm-buildings').filter(b => /* Do not match newly created objects */ b.feat.properties.id.indexOf('-') < 0)[0]?.feat?.properties ?? {}",
"_embedding_nr=feat.get('_embedded_in')['addr:housenumber']+(feat.get('_embedded_in')['addr:unit'] ?? '')", "_embedding_nr=feat.get('_embedded_in')['addr:housenumber']+(feat.get('_embedded_in')['addr:unit'] ?? '')",
"_embedding_street=feat.get('_embedded_in')['addr:street']", "_embedding_street=feat.get('_embedded_in')['addr:street']",
"_embedding_id=feat.get('_embedded_in').id", "_embedding_id=feat.get('_embedded_in').id",
"_closeby_addresses=feat.closestn('address',10,undefined,50).map(f => f.feat).filter(addr => addr.properties['addr:street'] == feat.properties['STRAATNM'] && feat.properties['HNRLABEL'] == addr.properties['addr:housenumber'] + (addr.properties['addr:unit']??'') ).length", "_closeby_addresses=feat.closestn('address',10,undefined,50).map(f => f.feat).filter(addr => addr.properties['addr:street'] == feat.properties['STRAATNM'] && feat.properties['HNRLABEL'] == addr.properties['addr:housenumber'] + (addr.properties['addr:unit']??'') ).length",
"_has_identical_closeby_address=feat.get('_closeby_addresses') >= 1 ? 'yes' : 'no'", "_has_identical_closeby_address=feat.get('_closeby_addresses') >= 1 ? 'yes' : 'no'",
"_embedded_in_grb=feat.overlapWith('GRB')[0]?.feat?.properties ?? {}", "_embedded_in_grb=feat.overlapWith('grb')[0]?.feat?.properties ?? {}",
"_embedding_nr_grb=feat.get('_embedded_in_grb')['addr:housenumber']", "_embedding_nr_grb=feat.get('_embedded_in_grb')['addr:housenumber']",
"_embedding_street_grb=feat.get('_embedded_in_grb')['addr:street']" "_embedding_street_grb=feat.get('_embedded_in_grb')['addr:street']"
], ],
@ -434,7 +452,7 @@
}, },
{ {
"id": "import-button", "id": "import-button",
"render": "{import_button(address, addr:street=$STRAATNM; addr:housenumber=$_HNRLABEL,Voeg dit adres als een nieuw adrespunt toe,,OSM-buildings,5)}", "render": "{import_button(address, addr:street=$STRAATNM; addr:housenumber=$_HNRLABEL,Voeg dit adres als een nieuw adrespunt toe,,osm-buildings,5)}",
"condition": { "condition": {
"and": [ "and": [
"_embedding_id!=", "_embedding_id!=",
@ -451,10 +469,15 @@
} }
}, },
{ {
"id": "GRB", "id": "grb",
"description": "Geometry which comes from GRB with tools to import them", "description": "Geometry which comes from GRB with tools to import them",
"source": { "source": {
"osmTags": "HUISNR~*", "osmTags": {
"and": [
"HUISNR~*",
"man_made!=mast"
]
},
"geoJson": "https://betadata.grbosm.site/grb?bbox={x_min},{y_min},{x_max},{y_max}", "geoJson": "https://betadata.grbosm.site/grb?bbox={x_min},{y_min},{x_max},{y_max}",
"geoJsonZoomLevel": 18, "geoJsonZoomLevel": 18,
"mercatorCrs": true, "mercatorCrs": true,
@ -463,7 +486,7 @@
"name": "GRB geometries", "name": "GRB geometries",
"title": "GRB outline", "title": "GRB outline",
"calculatedTags": [ "calculatedTags": [
"_overlaps_with_buildings=feat.overlapWith('OSM-buildings')", "_overlaps_with_buildings=feat.overlapWith('osm-buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)",
"_overlaps_with=feat.get('_overlaps_with_buildings').filter(f => f.overlap > 1 /* square meter */ )[0] ?? ''", "_overlaps_with=feat.get('_overlaps_with_buildings').filter(f => f.overlap > 1 /* square meter */ )[0] ?? ''",
"_overlap_absolute=feat.get('_overlaps_with')?.overlap", "_overlap_absolute=feat.get('_overlaps_with')?.overlap",
"_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_surface')) ", "_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_surface')) ",
@ -484,7 +507,7 @@
"tagRenderings": [ "tagRenderings": [
{ {
"id": "Import-button", "id": "Import-button",
"render": "{import_way_button(OSM-buildings,building=$building;man_made=$man_made; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref; addr:street=$addr:street; addr:housenumber=$addr:housenumber; building:min_level=$_building:min_level, Upload this building to OpenStreetMap,,_is_part_of_building=true,1,_moveable=true)}", "render": "{import_way_button(osm-buildings,building=$building;man_made=$man_made; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref; addr:street=$addr:street; addr:housenumber=$addr:housenumber; building:min_level=$_building:min_level, Upload this building to OpenStreetMap,,_is_part_of_building=true,1,_moveable=true)}",
"mappings": [ "mappings": [
{ {
"#": "Hide import button if intersection with other objects are detected", "#": "Hide import button if intersection with other objects are detected",
@ -501,11 +524,11 @@
"addr:housenumber~*" "addr:housenumber~*"
] ]
}, },
"then": "{conflate_button(OSM-buildings,building=$_target_building_type; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref; addr:street=$addr:street; addr:housenumber=$addr:housenumber, Replace the geometry in OpenStreetMap and add the address,,_osm_obj:id)}" "then": "{conflate_button(osm-buildings,building=$_target_building_type; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref; addr:street=$addr:street; addr:housenumber=$addr:housenumber, Replace the geometry in OpenStreetMap and add the address,,_osm_obj:id)}"
}, },
{ {
"if": "_overlaps_with!=", "if": "_overlaps_with!=",
"then": "{conflate_button(OSM-buildings,building=$_target_building_type; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Replace the geometry in OpenStreetMap,,_osm_obj:id)}" "then": "{conflate_button(osm-buildings,building=$_target_building_type; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Replace the geometry in OpenStreetMap,,_osm_obj:id)}"
} }
] ]
}, },
@ -548,6 +571,12 @@
"_osm_obj:id~*", "_osm_obj:id~*",
"addr:street~*", "addr:street~*",
"addr:housenumber~*", "addr:housenumber~*",
{
"or": [
"addr:street~*",
"addr:housenumber~*"
]
},
{ {
"or": [ "or": [
"addr:street!:={_osm_obj:addr:street}", "addr:street!:={_osm_obj:addr:street}",

View file

@ -31,7 +31,7 @@
}, },
"layers": [ "layers": [
{ {
"id": "OSM-buildings-fixme", "id": "osm-buildings-fixme",
"name": "OSM-buildings with a fixme", "name": "OSM-buildings with a fixme",
"source": { "source": {
"osmTags": { "osmTags": {

View file

@ -42,7 +42,7 @@
} }
], ],
"calculatedTags": [ "calculatedTags": [
"_overlapping=Number(feat.properties.zoom) >= 14 ? feat.overlapWith('OSM-buildings').map(ff => ff.feat.properties) : undefined", "_overlapping=Number(feat.properties.zoom) >= 14 ? feat.overlapWith('osm-buildings').map(ff => ff.feat.properties) : undefined",
"_applicable=feat.get('_overlapping').filter(p => (p._spelling_is_correct === 'true') && (p._singular_import === 'true')).map(p => p.id)", "_applicable=feat.get('_overlapping').filter(p => (p._spelling_is_correct === 'true') && (p._singular_import === 'true')).map(p => p.id)",
"_applicable_count=feat.get('_applicable')?.length" "_applicable_count=feat.get('_applicable')?.length"
], ],
@ -67,7 +67,7 @@
}, },
{ {
"id": "autoapply", "id": "autoapply",
"render": "{auto_apply(OSM-buildings, _applicable, apply_streetname, Automatically add all missing streetnames on buildings in view)}" "render": "{auto_apply(osm-buildings, _applicable, apply_streetname, Automatically add all missing streetnames on buildings in view)}"
} }
] ]
} }
@ -89,7 +89,7 @@
} }
}, },
{ {
"id": "OSM-buildings", "id": "osm-buildings",
"name": "Alle OSM-gebouwen met een huisnummer en zonder straat", "name": "Alle OSM-gebouwen met een huisnummer en zonder straat",
"source": { "source": {
"osmTags": { "osmTags": {

View file

@ -19,7 +19,7 @@
"layers": [ "layers": [
"street_lamps", "street_lamps",
{ {
"id": "Assen", "id": "assen",
"name": "Dataset Assen", "name": "Dataset Assen",
"source": { "source": {
"osmTags": "Lichtmastnummer~*", "osmTags": "Lichtmastnummer~*",

View file

@ -198,7 +198,8 @@
"downloadAsPdf": "Download a PDF of the current map", "downloadAsPdf": "Download a PDF of the current map",
"downloadAsPdfHelper": "Ideal to print the current map", "downloadAsPdfHelper": "Ideal to print the current map",
"downloadGeojson": "Download visible data as GeoJSON", "downloadGeojson": "Download visible data as GeoJSON",
"downloadGpx": "Download as GPX-file", "downloadFeatureAsGpx": "Download as GPX-file",
"downloadFeatureAsGeojson": "Download as GeoJson-file",
"downloadGpxHelper": "A GPX-file can be used with most navigation devices and applications", "downloadGpxHelper": "A GPX-file can be used with most navigation devices and applications",
"uploadGpx": "Upload your track to OpenStreetMap", "uploadGpx": "Upload your track to OpenStreetMap",
"exporting": "Exporting…", "exporting": "Exporting…",

9528
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -52,11 +52,11 @@
"license": "GPL", "license": "GPL",
"dependencies": { "dependencies": {
"@babel/preset-env": "7.13.8", "@babel/preset-env": "7.13.8",
"@turf/buffer": "^6.3.0", "@turf/buffer": "^6.5.0",
"@turf/collect": "^6.3.0", "@turf/collect": "^6.5.0",
"@turf/distance": "^6.3.0", "@turf/distance": "^6.5.0",
"@turf/length": "^6.3.0", "@turf/length": "^6.5.0",
"@turf/turf": "^6.3.0", "@turf/turf": "^6.5.0",
"@types/jquery": "^3.5.5", "@types/jquery": "^3.5.5",
"@types/leaflet-markercluster": "^1.0.3", "@types/leaflet-markercluster": "^1.0.3",
"@types/leaflet-providers": "^1.2.0", "@types/leaflet-providers": "^1.2.0",

View file

@ -3,6 +3,7 @@ import {equal} from "assert";
import T from "./TestHelper"; import T from "./TestHelper";
import {GeoOperations} from "../Logic/GeoOperations"; import {GeoOperations} from "../Logic/GeoOperations";
import {BBox} from "../Logic/BBox"; import {BBox} from "../Logic/BBox";
import * as turf from "@turf/turf"
export default class GeoOperationsSpec extends T { export default class GeoOperationsSpec extends T {
@ -187,7 +188,7 @@ export default class GeoOperationsSpec extends T {
], ],
["Regression test: intersection/overlap", () => { ["Regression test: intersection/overlap", () => {
const polyGrb ={ const polyGrb = {
"type": "Feature", "type": "Feature",
"properties": { "properties": {
"osm_id": "25189153", "osm_id": "25189153",
@ -351,10 +352,15 @@ export default class GeoOperationsSpec extends T {
} }
} }
const p0 = turf.polygon(polyGrb.geometry.coordinates)
Assert.notEqual(p0, null)
const p1 = turf.polygon(polyHouse.geometry.coordinates)
Assert.notEqual(p1, null)
const overlaps = GeoOperations.calculateOverlap(polyGrb, [polyHouse]) const overlaps = GeoOperations.calculateOverlap(polyGrb, [polyHouse])
Assert.equal(overlaps.length, 1) Assert.equal(overlaps.length, 0)
const overlapsRev= GeoOperations.calculateOverlap(polyHouse, [polyGrb]) const overlapsRev = GeoOperations.calculateOverlap(polyHouse, [polyGrb])
Assert.equal(overlaps.length, 1) Assert.equal(overlapsRev.length, 0)
}] }]
] ]

View file

@ -1,9 +1,9 @@
export default class T { export default class T {
public readonly name: string; public readonly name: string;
private readonly _tests: [string, (() => void)][]; private readonly _tests: [string, (() => (void | Promise<void>))][];
constructor(testsuite: string, tests: [string, () => void][]) { constructor(testsuite: string, tests: [string, () => (Promise<void> | void)][]) {
this.name = testsuite this.name = testsuite
this._tests = tests; this._tests = tests;
} }
@ -56,11 +56,18 @@ export default class T {
* Returns an empty list if successful * Returns an empty list if successful
* @constructor * @constructor
*/ */
public Run(): ({ testsuite: string, name: string, msg: string } []) { public Run(): { testsuite: string, name: string, msg: string } [] {
const failures: { testsuite: string, name: string, msg: string } [] = [] const failures: { testsuite: string, name: string, msg: string } [] = []
for (const [name, test] of this._tests) { for (const [name, test] of this._tests) {
try { try {
test(); const r = test()
if (r instanceof Promise) {
r.catch(e => {
console.log("ASYNC ERROR: ", e, e.stack)
failures.push({testsuite: this.name, name: name, msg: "" + e});
});
}
} catch (e) { } catch (e) {
console.log("ERROR: ", e, e.stack) console.log("ERROR: ", e, e.stack)
failures.push({testsuite: this.name, name: name, msg: "" + e}); failures.push({testsuite: this.name, name: name, msg: "" + e});