forked from MapComplete/MapComplete
		
	Merge develop
This commit is contained in:
		
						commit
						ac1b4a010c
					
				
					 40 changed files with 5706 additions and 4746 deletions
				
			
		|  | @ -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. | ||||||
|   |   | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  | @ -149,6 +150,16 @@ Information about the last edit of this object. | ||||||
| 
 | 
 | ||||||
| Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' and 'generic_key:right:property' (and similar for sidewalk tagging). Note that this rewritten tags _will be reuploaded on a change_. To prevent to much unrelated retagging, this is only enabled if the layer has at least some lineRenderings with offset defined | Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' and 'generic_key:right:property' (and similar for sidewalk tagging). Note that this rewritten tags _will be reuploaded on a change_. To prevent to much unrelated retagging, this is only enabled if the layer has at least some lineRenderings with offset defined | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### _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  | ||||||
|  |  | ||||||
|  | @ -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  | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|  |  | ||||||
|  | @ -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)) |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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;     | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|             } |             } | ||||||
|  |             throw "CalculateIntersection fallthrough: can not calculate an intersection between features" | ||||||
| 
 | 
 | ||||||
|         } catch (exception) { |  | ||||||
|             console.warn("EXCEPTION CAUGHT WHILE INTERSECTING: ", exception,"\nThe considered objects are",feature, otherFeature); |  | ||||||
|             return undefined |  | ||||||
|         } |  | ||||||
|         return undefined; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
| 
 | 
 | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |          | ||||||
|  |         // Second loop: figure out which point moves where without creating conflicts
 | ||||||
|         let conflictFree = true; |         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++) { | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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,92 +208,170 @@ 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
 |  | ||||||
|         const splitted = this.wayToReplaceId.split("/"); |  | ||||||
|         const type = splitted[0]; |  | ||||||
|         const idN = Number(splitted[1]); |  | ||||||
|         if (idN < 0 || type !== "way") { |  | ||||||
|             throw "Invalid ID to conflate: " + this.wayToReplaceId |  | ||||||
|         } |  | ||||||
|         const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`; |  | ||||||
|         const rawData = await Utils.downloadJsonCached(url, 1000) |  | ||||||
|         const 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) |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |         let parsed: OsmObject[]; | ||||||
|  |         { | ||||||
|  |             // Gather the needed OsmObjects
 | ||||||
|  |             const splitted = this.wayToReplaceId.split("/"); | ||||||
|  |             const type = splitted[0]; | ||||||
|  |             const idN = Number(splitted[1]); | ||||||
|  |             if (idN < 0 || type !== "way") { | ||||||
|  |                 throw "Invalid ID to conflate: " + this.wayToReplaceId | ||||||
|  |             } | ||||||
|  |             const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`; | ||||||
|  |             const rawData = await Utils.downloadJsonCached(url, 1000) | ||||||
|  |             parsed = OsmObject.ParseObjects(rawData.elements); | ||||||
|  |         } | ||||||
|  |         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) { | ||||||
|         for (let i = 0; i < this.targetCoordinates.length; i++) { |             const nodeDistances = this.targetCoordinates.map(_ => undefined) | ||||||
|             const target = this.targetCoordinates[i]; |             for (let i = 0; i < this.targetCoordinates.length; i++) { | ||||||
|             let closestDistance = undefined |                 if (this.identicalTo[i] !== undefined) { | ||||||
|             let closestId = undefined; |                     continue; | ||||||
|             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 |  | ||||||
|                 } |                 } | ||||||
|  |                 const targetCoordinate = this.targetCoordinates[i]; | ||||||
|  |                 const cp = node.centerpoint() | ||||||
|  |                 nodeDistances[i] = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]]) | ||||||
|             } |             } | ||||||
|             closestIds.push(closestId) |             distances.set(node.id, nodeDistances) | ||||||
|             distances.push(closestDistance) |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Next step: every closestId can only occur once in the list
 |         /** | ||||||
|         // We skip the ones which are identical
 |          * Then, we search the node that has to move the least distance and add this as mapping. | ||||||
|         console.log("Erasing double ids") |          * We do this until no points are left | ||||||
|         for (let i = 0; i < closestIds.length; i++) { |          */ | ||||||
|             if (this.identicalTo[i] !== undefined) { |         let candidate: number; | ||||||
|                 closestIds[i] = closestIds[this.identicalTo[i]] |         let moveDistance: number; | ||||||
|                 continue |         const closestIds = this.targetCoordinates.map(_ => undefined) | ||||||
|             } |         /** | ||||||
|             const closestId = closestIds[i] |          * The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates | ||||||
|             for (let j = i + 1; j < closestIds.length; j++) { |          */ | ||||||
|                 if (this.identicalTo[j] !== undefined) { |         const unusedIds = [] | ||||||
|                     continue |         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 | ||||||
|                 } |                 } | ||||||
|                 const otherClosestId = closestIds[j] |             }) | ||||||
|                 if (closestId !== otherClosestId) { | 
 | ||||||
|                     continue |             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; | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|                 // We have two occurences of 'closestId' - we only keep the closest instance!
 | 
 | ||||||
|                 const di = distances[i] |                 // This candidates role is done, it can be removed from the distance matrix
 | ||||||
|                 const dj = distances[j] |                 distances.delete(candidate) | ||||||
|                 if (di < dj) { | 
 | ||||||
|                     closestIds[j] = undefined |                 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) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         const osmWay = <OsmWay>parsed[parsed.length - 1] |         // If there are still unused values in 'distances', they are definitively unused
 | ||||||
|         if (osmWay.type !== "way") { |         distances.forEach((_, nodeId) => { | ||||||
|             throw "WEIRD: expected an OSM-way as last element here!" |             unusedIds.push(nodeId) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         { | ||||||
|  |             // Some extra data is included for rendering
 | ||||||
|  |             const osmWay = <OsmWay>parsed[parsed.length - 1] | ||||||
|  |             if (osmWay.type !== "way") { | ||||||
|  |                 throw "WEIRD: expected an OSM-way as last element here!" | ||||||
|  |             } | ||||||
|  |             const allNodesById = new Map<number, OsmNode>() | ||||||
|  |             for (const node of allNodes) { | ||||||
|  |                 allNodesById.set(node.id, <OsmNode>node) | ||||||
|  |             } | ||||||
|  |             return {closestIds, allNodesById, osmWay, detachedNodeIds: unusedIds}; | ||||||
|         } |         } | ||||||
|         return {closestIds, allNodesById, osmWay}; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -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 | ||||||
| 
 | 
 | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -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) { | ||||||
|  |  | ||||||
|  | @ -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); | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 = { | ||||||
|  |  | ||||||
|  | @ -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,10 +112,7 @@ 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) |           const iconStyle = "height: 1.5rem; width: auto" | ||||||
|         // Reset after 15s
 |  | ||||||
|         josmState.stabilized(15000).addCallbackD(_ => josmState.setData(undefined)) |  | ||||||
|         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, { | ||||||
|                 url: "https://liberapay.com/pietervdvn/", |                 url: "https://liberapay.com/pietervdvn/", | ||||||
|  | @ -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())) | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
|         ), |         ), | ||||||
|  |  | ||||||
|  | @ -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, "")]) | ||||||
|                     } |                     } | ||||||
|                 }) |                 }) | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  | @ -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,19 +366,19 @@ 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 | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  | @ -420,14 +429,14 @@ defaultValue: "0.1" | ||||||
|             } |             } | ||||||
|             mergeConfigs.push(mergeConfig) |             mergeConfigs.push(mergeConfig) | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
| 
 | 
 | ||||||
|         const moveOsmPointIfTags = args["move_osm_point_if"]?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) |         const moveOsmPointIfTags = args["move_osm_point_if"]?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) | ||||||
| 
 | 
 | ||||||
|         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 | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -145,7 +145,8 @@ export default class ShowDataLayer { | ||||||
|             pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng), |             pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng), | ||||||
|             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,11 +189,13 @@ export default class ShowDataLayer { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (options.zoomToFeatures ?? false) { |         if (options.zoomToFeatures ?? false) { | ||||||
|             try { |             if(this.geoLayer.getLayers().length > 0){ | ||||||
|                 const bounds = this.geoLayer.getBounds() |                 try { | ||||||
|                 mp.fitBounds(bounds, {animate: false}) |                     const bounds = this.geoLayer.getBounds() | ||||||
|             } catch (e) { |                     mp.fitBounds(bounds, {animate: false}) | ||||||
|                 console.debug("Invalid bounds", e) |                 } catch (e) { | ||||||
|  |                     console.debug("Invalid bounds", e) | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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", | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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?", | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -184,9 +184,16 @@ | ||||||
|           } |           } | ||||||
|         ] |         ] | ||||||
|       }, |       }, | ||||||
|  |       "fill": "no", | ||||||
|       "width": { |       "width": { | ||||||
|         "render": "8" |         "render": "8", | ||||||
|  |         "mappings": [ | ||||||
|  |           { | ||||||
|  |             "if": "_geometry:type=Polygon", | ||||||
|  |             "then": "16" | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -543,7 +543,7 @@ | ||||||
|       "condition": "cuisine=friture" |       "condition": "cuisine=friture" | ||||||
|     }, |     }, | ||||||
|     "service:electricity", |     "service:electricity", | ||||||
|     "dog-access" |     "dog-access","reviews" | ||||||
|   ], |   ], | ||||||
|   "filter": [ |   "filter": [ | ||||||
|     { |     { | ||||||
|  | @ -679,4 +679,4 @@ | ||||||
|   "description": { |   "description": { | ||||||
|     "en": "A layer showing restaurants and fast-food amenities (with a special rendering for friteries)" |     "en": "A layer showing restaurants and fast-food amenities (with a special rendering for friteries)" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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": { | ||||||
|  |  | ||||||
|  | @ -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~*", | ||||||
|  |  | ||||||
|  | @ -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}", | ||||||
|  |  | ||||||
|  | @ -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": { | ||||||
|  |  | ||||||
|  | @ -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": { | ||||||
|  |  | ||||||
|  | @ -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~*", | ||||||
|  |  | ||||||
|  | @ -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…", | ||||||
|  |  | ||||||
							
								
								
									
										9564
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										9564
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										10
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
										
									
									
									
								
							|  | @ -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", | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
| 
 | 
 | ||||||
|                 }] |                 }] | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|  | @ -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}); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue