forked from MapComplete/MapComplete
Add 'CreateNewWayWithNodeReuse'-action, use it in the GRB-theme
This commit is contained in:
parent
4e3f408d53
commit
63acca1638
10 changed files with 473 additions and 133 deletions
|
@ -58,10 +58,15 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
private readonly _layoutToUse: LayoutConfig;
|
private readonly _layoutToUse: LayoutConfig;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
currentGPSLocation: UIEventSource<Coordinates>,
|
state: {
|
||||||
leafletMap: UIEventSource<any>,
|
currentGPSLocation: UIEventSource<Coordinates>,
|
||||||
layoutToUse: LayoutConfig
|
leafletMap: UIEventSource<any>,
|
||||||
|
layoutToUse: LayoutConfig,
|
||||||
|
featureSwitchGeolocation: UIEventSource<boolean>
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
|
const currentGPSLocation = state.currentGPSLocation
|
||||||
|
const leafletMap = state.leafletMap
|
||||||
const hasLocation = currentGPSLocation.map(
|
const hasLocation = currentGPSLocation.map(
|
||||||
(location) => location !== undefined
|
(location) => location !== undefined
|
||||||
);
|
);
|
||||||
|
@ -122,7 +127,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
this._previousLocationGrant = previousLocationGrant;
|
this._previousLocationGrant = previousLocationGrant;
|
||||||
this._currentGPSLocation = currentGPSLocation;
|
this._currentGPSLocation = currentGPSLocation;
|
||||||
this._leafletMap = leafletMap;
|
this._leafletMap = leafletMap;
|
||||||
this._layoutToUse = layoutToUse;
|
this._layoutToUse = state.layoutToUse;
|
||||||
this._hasLocation = hasLocation;
|
this._hasLocation = hasLocation;
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
@ -167,7 +172,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
|
|
||||||
const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon")
|
const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon")
|
||||||
|
|
||||||
this.init(false, !latLonGiven);
|
this.init(false, !latLonGiven && state.featureSwitchGeolocation.data);
|
||||||
|
|
||||||
isLocked.addCallbackAndRunD(isLocked => {
|
isLocked.addCallbackAndRunD(isLocked => {
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
|
@ -208,7 +213,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private init(askPermission: boolean, forceZoom: boolean) {
|
private init(askPermission: boolean, zoomToLocation: boolean) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
if (self._isActive.data) {
|
if (self._isActive.data) {
|
||||||
|
@ -222,7 +227,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
?.then(function (status) {
|
?.then(function (status) {
|
||||||
console.log("Geolocation permission is ", status.state);
|
console.log("Geolocation permission is ", status.state);
|
||||||
if (status.state === "granted") {
|
if (status.state === "granted") {
|
||||||
self.StartGeolocating(forceZoom);
|
self.StartGeolocating(zoomToLocation);
|
||||||
}
|
}
|
||||||
self._permission.setData(status.state);
|
self._permission.setData(status.state);
|
||||||
status.onchange = function () {
|
status.onchange = function () {
|
||||||
|
@ -234,10 +239,10 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (askPermission) {
|
if (askPermission) {
|
||||||
self.StartGeolocating(forceZoom);
|
self.StartGeolocating(zoomToLocation);
|
||||||
} else if (this._previousLocationGrant.data === "granted") {
|
} else if (this._previousLocationGrant.data === "granted") {
|
||||||
this._previousLocationGrant.setData("");
|
this._previousLocationGrant.setData("");
|
||||||
self.StartGeolocating(forceZoom);
|
self.StartGeolocating(zoomToLocation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,21 +3,6 @@ import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||||
import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject";
|
import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject";
|
||||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource";
|
import SimpleFeatureSource from "../Sources/SimpleFeatureSource";
|
||||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||||
import {TagsFilter} from "../../Tags/TagsFilter";
|
|
||||||
import OsmChangeAction from "../../Osm/Actions/OsmChangeAction";
|
|
||||||
import StaticFeatureSource from "../Sources/StaticFeatureSource";
|
|
||||||
import {OsmConnection} from "../../Osm/OsmConnection";
|
|
||||||
import {GeoOperations} from "../../GeoOperations";
|
|
||||||
import {Utils} from "../../../Utils";
|
|
||||||
import {UIEventSource} from "../../UIEventSource";
|
|
||||||
import {BBox} from "../../BBox";
|
|
||||||
import FeaturePipeline from "../FeaturePipeline";
|
|
||||||
import {Tag} from "../../Tags/Tag";
|
|
||||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
|
||||||
import {ChangeDescription} from "../../Osm/Actions/ChangeDescription";
|
|
||||||
import CreateNewNodeAction from "../../Osm/Actions/CreateNewNodeAction";
|
|
||||||
import ChangeTagAction from "../../Osm/Actions/ChangeTagAction";
|
|
||||||
import {And} from "../../Tags/And";
|
|
||||||
|
|
||||||
|
|
||||||
export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> {
|
export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> {
|
||||||
|
@ -35,69 +20,6 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a list of coordinates, will search already existing OSM-points to snap onto.
|
|
||||||
* Either the geometry will be moved OR the existing point will be moved, depending on configuration and tags.
|
|
||||||
* This requires the 'type_node'-layer to be activated
|
|
||||||
*/
|
|
||||||
public static MergePoints(
|
|
||||||
state: {
|
|
||||||
filteredLayers: UIEventSource<FilteredLayer[]>,
|
|
||||||
featurePipeline: FeaturePipeline,
|
|
||||||
layoutToUse: LayoutConfig
|
|
||||||
},
|
|
||||||
newGeometryLngLats: [number, number][],
|
|
||||||
configs: ConflationConfig[],
|
|
||||||
) {
|
|
||||||
const typeNode = state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0]
|
|
||||||
if (typeNode === undefined) {
|
|
||||||
throw "Type Node layer is not defined. Add 'type_node' as layer to your layerconfig to use this feature"
|
|
||||||
}
|
|
||||||
|
|
||||||
const bbox = new BBox(newGeometryLngLats)
|
|
||||||
const bbox_padded = bbox.pad(1.2)
|
|
||||||
const allNodes: any[] = [].concat(...state.featurePipeline.GetFeaturesWithin("type_node", bbox).map(tile => tile.filter(
|
|
||||||
feature => bbox_padded.contains(GeoOperations.centerpointCoordinates(feature))
|
|
||||||
)))
|
|
||||||
// The strategy: for every point of the new geometry, we search a point that is closeby and matches
|
|
||||||
// If multiple options match, we choose the most optimal (aka closest)
|
|
||||||
|
|
||||||
const maxDistance = Math.max(...configs.map(c => c.withinRangeOfM))
|
|
||||||
for (const coordinate of newGeometryLngLats) {
|
|
||||||
|
|
||||||
let closestNode = undefined;
|
|
||||||
let closestNodeDistance = undefined
|
|
||||||
for (const node of allNodes) {
|
|
||||||
const d = GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(node), coordinate)
|
|
||||||
if (d > maxDistance) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let matchesSomeConfig = false
|
|
||||||
for (const config of configs) {
|
|
||||||
if (d > config.withinRangeOfM) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (!config.ifMatches.matchesProperties(node.properties)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
matchesSomeConfig = true;
|
|
||||||
}
|
|
||||||
if (!matchesSomeConfig) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (closestNode === undefined || closestNodeDistance > d) {
|
|
||||||
closestNode = node;
|
|
||||||
closestNodeDistance = d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public handleOsmJson(osmJson: any, tileId: number) {
|
public handleOsmJson(osmJson: any, tileId: number) {
|
||||||
|
|
||||||
const allObjects = OsmObject.ParseObjects(osmJson.elements)
|
const allObjects = OsmObject.ParseObjects(osmJson.elements)
|
||||||
|
@ -143,8 +65,3 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConflationConfig {
|
|
||||||
withinRangeOfM: number,
|
|
||||||
ifMatches: TagsFilter,
|
|
||||||
mode: "reuse_osm_point" | "move_osm_point"
|
|
||||||
}
|
|
|
@ -304,7 +304,7 @@ export class GeoOperations {
|
||||||
return [x, y];
|
return [x, y];
|
||||||
}
|
}
|
||||||
|
|
||||||
//Converts XY point from (Spherical) Web Mercator EPSG:3785 (unofficially EPSG:900913) to lat/lon in WGS84 Datum
|
//Converts XY point from (Spherical) Web Mercator EPSG:3785 (unofficially EPSG:900913) to lat/lon in WGS84 Datum
|
||||||
public static Convert900913ToWgs84(lonLat: [number, number]): [number, number] {
|
public static Convert900913ToWgs84(lonLat: [number, number]): [number, number] {
|
||||||
const lon = lonLat[0]
|
const lon = lonLat[0]
|
||||||
const lat = lonLat[1]
|
const lat = lonLat[1]
|
||||||
|
@ -410,6 +410,31 @@ export class GeoOperations {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to remove points which do not contribute much to the general outline.
|
||||||
|
* Points for which the angle is ~ 180° are removed
|
||||||
|
* @param coordinates
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public static SimplifyCoordinates(coordinates: [number, number][]){
|
||||||
|
const newCoordinates = []
|
||||||
|
for (let i = 1; i < coordinates.length - 1; i++){
|
||||||
|
const coordinate = coordinates[i];
|
||||||
|
const prev = coordinates[i - 1]
|
||||||
|
const next = coordinates[i + 1]
|
||||||
|
const b0 = turf.bearing(prev, coordinate, {final: true})
|
||||||
|
const b1 = turf.bearing(coordinate, next)
|
||||||
|
|
||||||
|
const diff = Math.abs(b1 - b0)
|
||||||
|
if(diff < 2){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newCoordinates.push(coordinate)
|
||||||
|
}
|
||||||
|
return newCoordinates
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
313
Logic/Osm/Actions/CreateWayWithPointReuseAction.ts
Normal file
313
Logic/Osm/Actions/CreateWayWithPointReuseAction.ts
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
import OsmChangeAction from "./OsmChangeAction";
|
||||||
|
import {Tag} from "../../Tags/Tag";
|
||||||
|
import {Changes} from "../Changes";
|
||||||
|
import {ChangeDescription} from "./ChangeDescription";
|
||||||
|
import FeaturePipelineState from "../../State/FeaturePipelineState";
|
||||||
|
import {BBox} from "../../BBox";
|
||||||
|
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||||
|
import {GeoOperations} from "../../GeoOperations";
|
||||||
|
import FeatureSource from "../../FeatureSource/FeatureSource";
|
||||||
|
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource";
|
||||||
|
import CreateNewNodeAction from "./CreateNewNodeAction";
|
||||||
|
import CreateNewWayAction from "./CreateNewWayAction";
|
||||||
|
|
||||||
|
|
||||||
|
export interface MergePointConfig {
|
||||||
|
withinRangeOfM: number,
|
||||||
|
ifMatches: TagsFilter,
|
||||||
|
mode: "reuse_osm_point" | "move_osm_point"
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CoordinateInfo {
|
||||||
|
lngLat: [number, number],
|
||||||
|
identicalTo?: number,
|
||||||
|
closebyNodes?: {
|
||||||
|
d: number,
|
||||||
|
node: any,
|
||||||
|
config: MergePointConfig
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
|
||||||
|
*/
|
||||||
|
export default class CreateWayWithPointReuseAction extends OsmChangeAction {
|
||||||
|
private readonly _tags: Tag[];
|
||||||
|
/**
|
||||||
|
* lngLat-coordinates
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _coordinateInfo: CoordinateInfo[];
|
||||||
|
private _state: FeaturePipelineState;
|
||||||
|
private _config: MergePointConfig[];
|
||||||
|
|
||||||
|
constructor(tags: Tag[],
|
||||||
|
coordinates: [number, number][],
|
||||||
|
state: FeaturePipelineState,
|
||||||
|
config: MergePointConfig[]
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this._tags = tags;
|
||||||
|
this._state = state;
|
||||||
|
this._config = config;
|
||||||
|
this._coordinateInfo = this.CalculateClosebyNodes(coordinates);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPreview(): Promise<FeatureSource> {
|
||||||
|
|
||||||
|
const features = []
|
||||||
|
let geometryMoved = false;
|
||||||
|
for (let i = 0; i < this._coordinateInfo.length; i++) {
|
||||||
|
const coordinateInfo = this._coordinateInfo[i];
|
||||||
|
if (coordinateInfo.identicalTo !== undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (coordinateInfo.closebyNodes === undefined || coordinateInfo.closebyNodes.length === 0) {
|
||||||
|
|
||||||
|
const newPoint = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: {
|
||||||
|
"newpoint": "yes",
|
||||||
|
id: "new-geometry-with-reuse-" + i
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: "Point",
|
||||||
|
coordinates: coordinateInfo.lngLat
|
||||||
|
}
|
||||||
|
};
|
||||||
|
features.push(newPoint)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const reusedPoint = coordinateInfo.closebyNodes[0]
|
||||||
|
if (reusedPoint.config.mode === "move_osm_point") {
|
||||||
|
const moveDescription = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: {
|
||||||
|
"move": "yes",
|
||||||
|
"osm-id": reusedPoint.node.properties.id,
|
||||||
|
"id": "new-geometry-move-existing" + i
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: "LineString",
|
||||||
|
coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
features.push(moveDescription)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// The geometry is moved
|
||||||
|
geometryMoved = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geometryMoved) {
|
||||||
|
|
||||||
|
const coords: [number, number][] = []
|
||||||
|
for (const info of this._coordinateInfo) {
|
||||||
|
if (info.identicalTo !== undefined) {
|
||||||
|
coords.push(coords[info.identicalTo])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.closebyNodes === undefined || info.closebyNodes.length === 0) {
|
||||||
|
coords.push(coords[info.identicalTo])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const closest = info.closebyNodes[0]
|
||||||
|
if (closest.config.mode === "reuse_osm_point") {
|
||||||
|
coords.push(closest.node.geometry.coordinates)
|
||||||
|
} else {
|
||||||
|
coords.push(info.lngLat)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const newGeometry = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: {
|
||||||
|
"resulting-geometry": "yes",
|
||||||
|
"id": "new-geometry"
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: "LineString",
|
||||||
|
coordinates: coords
|
||||||
|
}
|
||||||
|
}
|
||||||
|
features.push(newGeometry)
|
||||||
|
|
||||||
|
}
|
||||||
|
console.log("Preview:", features)
|
||||||
|
return new StaticFeatureSource(features, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||||
|
const theme = this._state.layoutToUse.id
|
||||||
|
const allChanges: ChangeDescription[] = []
|
||||||
|
const nodeIdsToUse: { lat: number, lon: number, nodeId?: number }[] = []
|
||||||
|
for (let i = 0; i < this._coordinateInfo.length; i++) {
|
||||||
|
const info = this._coordinateInfo[i]
|
||||||
|
const lat = info.lngLat[1]
|
||||||
|
const lon = info.lngLat[0]
|
||||||
|
|
||||||
|
if (info.identicalTo !== undefined) {
|
||||||
|
nodeIdsToUse.push(nodeIdsToUse[info.identicalTo])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (info.closebyNodes === undefined || info.closebyNodes[0] === undefined) {
|
||||||
|
const newNodeAction = new CreateNewNodeAction([], lat, lon, {
|
||||||
|
allowReuseOfPreviouslyCreatedPoints: true,
|
||||||
|
changeType: null,
|
||||||
|
theme
|
||||||
|
})
|
||||||
|
|
||||||
|
allChanges.push(...(await newNodeAction.CreateChangeDescriptions(changes)))
|
||||||
|
|
||||||
|
nodeIdsToUse.push({
|
||||||
|
lat, lon,
|
||||||
|
nodeId : newNodeAction.newElementIdNumber})
|
||||||
|
continue
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const closestPoint = info.closebyNodes[0]
|
||||||
|
const id = Number(closestPoint.node.properties.id.split("/")[1])
|
||||||
|
if(closestPoint.config.mode === "move_osm_point"){
|
||||||
|
allChanges.push({
|
||||||
|
type: "node",
|
||||||
|
id,
|
||||||
|
changes: {
|
||||||
|
lat, lon
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
theme,
|
||||||
|
changeType: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
nodeIdsToUse.push({lat, lon, nodeId: id})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, {
|
||||||
|
theme
|
||||||
|
})
|
||||||
|
|
||||||
|
allChanges.push(...(await newWay.Perform(changes)))
|
||||||
|
|
||||||
|
return allChanges
|
||||||
|
}
|
||||||
|
|
||||||
|
private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] {
|
||||||
|
|
||||||
|
const bbox = new BBox(coordinates)
|
||||||
|
const state = this._state
|
||||||
|
const allNodes = [].concat(...state.featurePipeline.GetFeaturesWithin("type_node", bbox.pad(1.2)))
|
||||||
|
const maxDistance = Math.max(...this._config.map(c => c.withinRangeOfM))
|
||||||
|
|
||||||
|
const coordinateInfo: {
|
||||||
|
lngLat: [number, number],
|
||||||
|
identicalTo?: number,
|
||||||
|
closebyNodes?: {
|
||||||
|
d: number,
|
||||||
|
node: any,
|
||||||
|
config: MergePointConfig
|
||||||
|
}[]
|
||||||
|
}[] = coordinates.map(_ => undefined)
|
||||||
|
|
||||||
|
for (let i = 0; i < coordinates.length; i++) {
|
||||||
|
|
||||||
|
if (coordinateInfo[i] !== undefined) {
|
||||||
|
// Already seen, probably a duplicate coordinate
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const coor = coordinates[i]
|
||||||
|
// Check closeby (and probably identical) point further in the coordinate list, mark them as duplicate
|
||||||
|
for (let j = i + 1; j < coordinates.length; j++) {
|
||||||
|
if (1000 * GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) {
|
||||||
|
coordinateInfo[j] = {
|
||||||
|
lngLat: coor,
|
||||||
|
identicalTo: i
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather the actual info for this point
|
||||||
|
|
||||||
|
// Lets search applicable points and determine the merge mode
|
||||||
|
const closebyNodes: {
|
||||||
|
d: number,
|
||||||
|
node: any,
|
||||||
|
config: MergePointConfig
|
||||||
|
}[] = []
|
||||||
|
for (const node of allNodes) {
|
||||||
|
const center = node.geometry.coordinates
|
||||||
|
const d = 1000 * GeoOperations.distanceBetween(coor, center)
|
||||||
|
if (d > maxDistance) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const config of this._config) {
|
||||||
|
if (d > config.withinRangeOfM) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!config.ifMatches.matchesProperties(node.properties)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
closebyNodes.push({node, d, config})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closebyNodes.sort((n0, n1) => {
|
||||||
|
return n0.d - n1.d
|
||||||
|
})
|
||||||
|
|
||||||
|
coordinateInfo[i] = {
|
||||||
|
identicalTo: undefined,
|
||||||
|
lngLat: coor,
|
||||||
|
closebyNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let conflictFree = true;
|
||||||
|
|
||||||
|
do {
|
||||||
|
conflictFree = true;
|
||||||
|
for (let i = 0; i < coordinateInfo.length; i++) {
|
||||||
|
|
||||||
|
const coorInfo = coordinateInfo[i]
|
||||||
|
if (coorInfo.identicalTo !== undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (coorInfo.closebyNodes === undefined || coorInfo.closebyNodes[0] === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = i + 1; j < coordinates.length; j++) {
|
||||||
|
const other = coordinateInfo[j]
|
||||||
|
if (other.closebyNodes === undefined || other.closebyNodes[0] === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other.closebyNodes[0].node === coorInfo.closebyNodes[0].node) {
|
||||||
|
conflictFree = false
|
||||||
|
// We have found a conflict!
|
||||||
|
// We only keep the closest point
|
||||||
|
if (other.closebyNodes[0].d > coorInfo.closebyNodes[0].d) {
|
||||||
|
other.closebyNodes.shift()
|
||||||
|
} else {
|
||||||
|
coorInfo.closebyNodes.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (!conflictFree)
|
||||||
|
|
||||||
|
|
||||||
|
return coordinateInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -19,7 +19,15 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
};
|
};
|
||||||
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
|
||||||
|
*/
|
||||||
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.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private readonly identicalTo: number[]
|
||||||
private readonly newTags: Tag[] | undefined;
|
private readonly newTags: Tag[] | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -46,13 +54,36 @@ 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.identicalTo = coordinates.map(_ => undefined)
|
||||||
|
|
||||||
|
for (let i = 0; i < coordinates.length; i++) {
|
||||||
|
if (this.identicalTo[i] !== undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (let j = i + 1; j < coordinates.length; j++) {
|
||||||
|
const d = 1000 * GeoOperations.distanceBetween(coordinates[i], coordinates[j])
|
||||||
|
if (d < 0.1) {
|
||||||
|
console.log("Identical coordinates detected: ", i, " and ", j, ": ", coordinates[i], coordinates[j], "distance is", d)
|
||||||
|
this.identicalTo[j] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
this.targetCoordinates = coordinates
|
this.targetCoordinates = coordinates
|
||||||
this.newTags = options.newTags
|
this.newTags = options.newTags
|
||||||
}
|
}
|
||||||
|
|
||||||
public async GetPreview(): Promise<FeatureSource> {
|
public async getPreview(): Promise<FeatureSource> {
|
||||||
const {closestIds, allNodesById} = await this.GetClosestIds();
|
const {closestIds, allNodesById} = await this.GetClosestIds();
|
||||||
|
console.log("Generating preview, identicals are ", )
|
||||||
const preview = closestIds.map((newId, i) => {
|
const preview = closestIds.map((newId, i) => {
|
||||||
|
if(this.identicalTo[i] !== undefined){
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (newId === undefined) {
|
if (newId === undefined) {
|
||||||
return {
|
return {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
|
@ -80,7 +111,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
return new StaticFeatureSource(preview, false)
|
return new StaticFeatureSource(Utils.NoNull(preview), false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,6 +123,11 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
const {closestIds, osmWay} = await this.GetClosestIds()
|
const {closestIds, osmWay} = await this.GetClosestIds()
|
||||||
|
|
||||||
for (let i = 0; i < closestIds.length; i++) {
|
for (let i = 0; i < closestIds.length; i++) {
|
||||||
|
if(this.identicalTo[i] !== undefined){
|
||||||
|
const j = this.identicalTo[i]
|
||||||
|
actualIdsToUse.push(actualIdsToUse[j])
|
||||||
|
continue
|
||||||
|
}
|
||||||
const closestId = closestIds[i];
|
const closestId = closestIds[i];
|
||||||
const [lon, lat] = this.targetCoordinates[i]
|
const [lon, lat] = this.targetCoordinates[i]
|
||||||
if (closestId === undefined) {
|
if (closestId === undefined) {
|
||||||
|
@ -161,7 +197,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
private async GetClosestIds(): Promise<{ closestIds: number[], allNodesById: Map<number, OsmNode>, osmWay: OsmWay }> {
|
private async GetClosestIds(): Promise<{ closestIds: number[], allNodesById: Map<number, OsmNode>, osmWay: OsmWay }> {
|
||||||
// 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: reuse points if they are the same in the target coordinates
|
// TODO FIXME: detect intersections with other ways if moved
|
||||||
const splitted = this.wayToReplaceId.split("/");
|
const splitted = this.wayToReplaceId.split("/");
|
||||||
const type = splitted[0];
|
const type = splitted[0];
|
||||||
const idN = Number(splitted[1]);
|
const idN = Number(splitted[1]);
|
||||||
|
@ -185,7 +221,8 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
|
|
||||||
const closestIds = []
|
const closestIds = []
|
||||||
const distances = []
|
const distances = []
|
||||||
for (const target of this.targetCoordinates) {
|
for (let i = 0; i < this.targetCoordinates.length; i++){
|
||||||
|
const target = this.targetCoordinates[i];
|
||||||
let closestDistance = undefined
|
let closestDistance = undefined
|
||||||
let closestId = undefined;
|
let closestId = undefined;
|
||||||
for (const osmNode of allNodes) {
|
for (const osmNode of allNodes) {
|
||||||
|
@ -202,9 +239,18 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next step: every closestId can only occur once in the list
|
// Next step: every closestId can only occur once in the list
|
||||||
|
// We skip the ones which are identical
|
||||||
|
console.log("Erasing double ids")
|
||||||
for (let i = 0; i < closestIds.length; i++) {
|
for (let i = 0; i < closestIds.length; i++) {
|
||||||
|
if(this.identicalTo[i] !== undefined){
|
||||||
|
closestIds[i] = closestIds[this.identicalTo[i]]
|
||||||
|
continue
|
||||||
|
}
|
||||||
const closestId = closestIds[i]
|
const closestId = closestIds[i]
|
||||||
for (let j = i + 1; j < closestIds.length; j++) {
|
for (let j = i + 1; j < closestIds.length; j++) {
|
||||||
|
if(this.identicalTo[j] !== undefined){
|
||||||
|
continue
|
||||||
|
}
|
||||||
const otherClosestId = closestIds[j]
|
const otherClosestId = closestIds[j]
|
||||||
if (closestId !== otherClosestId) {
|
if (closestId !== otherClosestId) {
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {Utils} from "../Utils";
|
||||||
|
|
||||||
export default class Constants {
|
export default class Constants {
|
||||||
|
|
||||||
public static vNumber = "0.12.1-beta";
|
public static vNumber = "0.12.2-beta";
|
||||||
public static ImgurApiKey = '7070e7167f0a25a'
|
public static ImgurApiKey = '7070e7167f0a25a'
|
||||||
public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2'
|
public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2'
|
||||||
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
||||||
|
|
|
@ -32,6 +32,10 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature
|
||||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
||||||
import BaseLayer from "../../Models/BaseLayer";
|
import BaseLayer from "../../Models/BaseLayer";
|
||||||
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
|
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
|
||||||
|
import FullNodeDatabaseSource from "../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||||
|
import CreateWayWithPointReuseAction from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||||
|
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction";
|
||||||
|
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
|
||||||
|
|
||||||
|
|
||||||
export interface ImportButtonState {
|
export interface ImportButtonState {
|
||||||
|
@ -282,7 +286,7 @@ export default class ImportButton extends Toggle {
|
||||||
importClicked: UIEventSource<boolean>): BaseUIElement {
|
importClicked: UIEventSource<boolean>): BaseUIElement {
|
||||||
|
|
||||||
const confirmationMap = Minimap.createMiniMap({
|
const confirmationMap = Minimap.createMiniMap({
|
||||||
allowMoving: false,
|
allowMoving: true,
|
||||||
background: o.state.backgroundLayer
|
background: o.state.backgroundLayer
|
||||||
})
|
})
|
||||||
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
|
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
|
||||||
|
@ -298,14 +302,14 @@ export default class ImportButton extends Toggle {
|
||||||
layers: o.state.filteredLayers
|
layers: o.state.filteredLayers
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let action : OsmChangeAction & {getPreview() : Promise<FeatureSource>}
|
||||||
|
|
||||||
const theme = o.state.layoutToUse.id
|
const theme = o.state.layoutToUse.id
|
||||||
|
|
||||||
|
|
||||||
const changes = o.state.changes
|
const changes = o.state.changes
|
||||||
let confirm: () => Promise<string>
|
let confirm: () => Promise<string>
|
||||||
if (o.conflationSettings !== undefined) {
|
if (o.conflationSettings !== undefined) {
|
||||||
|
|
||||||
let replaceGeometryAction = new ReplaceGeometryAction(
|
action = new ReplaceGeometryAction(
|
||||||
o.state,
|
o.state,
|
||||||
o.feature,
|
o.feature,
|
||||||
o.conflationSettings.conflateWayId,
|
o.conflationSettings.conflateWayId,
|
||||||
|
@ -315,40 +319,53 @@ export default class ImportButton extends Toggle {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
replaceGeometryAction.GetPreview().then(changePreview => {
|
|
||||||
new ShowDataLayer({
|
|
||||||
leafletMap: confirmationMap.leafletMap,
|
|
||||||
enablePopups: false,
|
|
||||||
zoomToFeatures: false,
|
|
||||||
features: changePreview,
|
|
||||||
allElements: o.state.allElements,
|
|
||||||
layerToShow: AllKnownLayers.sharedLayers.get("conflation")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
confirm = async () => {
|
confirm = async () => {
|
||||||
changes.applyAction (replaceGeometryAction)
|
changes.applyAction (action)
|
||||||
return o.feature.properties.id
|
return o.feature.properties.id
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
const geom = o.feature.geometry
|
||||||
|
let coordinates: [number, number][]
|
||||||
|
if (geom.type === "LineString") {
|
||||||
|
coordinates = geom.coordinates
|
||||||
|
} else if (geom.type === "Polygon") {
|
||||||
|
coordinates = geom.coordinates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
action = new CreateWayWithPointReuseAction(
|
||||||
|
o.newTags.data,
|
||||||
|
coordinates,
|
||||||
|
// @ts-ignore
|
||||||
|
o.state,
|
||||||
|
[{
|
||||||
|
withinRangeOfM: 1,
|
||||||
|
ifMatches: new Tag("_is_part_of_building","true"),
|
||||||
|
mode:"move_osm_point"
|
||||||
|
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
confirm = async () => {
|
confirm = async () => {
|
||||||
const geom = o.feature.geometry
|
changes.applyAction(action)
|
||||||
let coordinates: [number, number][]
|
return undefined
|
||||||
if (geom.type === "LineString") {
|
|
||||||
coordinates = geom.coordinates
|
|
||||||
} else if (geom.type === "Polygon") {
|
|
||||||
coordinates = geom.coordinates[0]
|
|
||||||
}
|
|
||||||
const action = new CreateNewWayAction(o.newTags.data, coordinates.map(lngLat => ({
|
|
||||||
lat: lngLat[1],
|
|
||||||
lon: lngLat[0]
|
|
||||||
})), {theme})
|
|
||||||
return action.newElementId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
action.getPreview().then(changePreview => {
|
||||||
|
new ShowDataLayer({
|
||||||
|
leafletMap: confirmationMap.leafletMap,
|
||||||
|
enablePopups: false,
|
||||||
|
zoomToFeatures: false,
|
||||||
|
features: changePreview,
|
||||||
|
allElements: o.state.allElements,
|
||||||
|
layerToShow: AllKnownLayers.sharedLayers.get("conflation")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const confirmButton = new SubtleButton(o.image(), o.message)
|
const confirmButton = new SubtleButton(o.image(), o.message)
|
||||||
confirmButton.onClick(async () => {
|
confirmButton.onClick(async () => {
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,9 +12,7 @@ export default class RightControls extends Combine {
|
||||||
constructor(state:MapState) {
|
constructor(state:MapState) {
|
||||||
|
|
||||||
const geolocatioHandler = new GeoLocationHandler(
|
const geolocatioHandler = new GeoLocationHandler(
|
||||||
state.currentGPSLocation,
|
state
|
||||||
state.leafletMap,
|
|
||||||
state.layoutToUse
|
|
||||||
)
|
)
|
||||||
|
|
||||||
new ShowDataLayer({
|
new ShowDataLayer({
|
||||||
|
|
|
@ -30,7 +30,17 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"width": "3",
|
"width": "3",
|
||||||
"color": "#00f"
|
"color": "#00f",
|
||||||
|
|
||||||
|
"dasharray": {
|
||||||
|
"render": "",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "resulting-geometry=yes",
|
||||||
|
"then": "6 6"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -29,6 +29,7 @@
|
||||||
"minzoom": 18
|
"minzoom": 18
|
||||||
},
|
},
|
||||||
"trackAllNodes": true,
|
"trackAllNodes": true,
|
||||||
|
"enableGeolocation": false,
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
"builtin": "type_node",
|
"builtin": "type_node",
|
||||||
|
@ -41,6 +42,13 @@
|
||||||
"_is_part_of_building_passage=feat.get('parent_ways')?.some(p => p.tunnel === 'building_passage') ?? false",
|
"_is_part_of_building_passage=feat.get('parent_ways')?.some(p => p.tunnel === 'building_passage') ?? false",
|
||||||
"_is_part_of_highway=!feat.get('is_part_of_building_passage') && (feat.get('parent_ways')?.some(p => p.highway !== undefined && p.highway !== '') ?? false)",
|
"_is_part_of_highway=!feat.get('is_part_of_building_passage') && (feat.get('parent_ways')?.some(p => p.highway !== undefined && p.highway !== '') ?? false)",
|
||||||
"_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? false"
|
"_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? false"
|
||||||
|
],
|
||||||
|
"mapRendering": [
|
||||||
|
{
|
||||||
|
"icon": "square:#00f",
|
||||||
|
"iconSize": "5,5,center",
|
||||||
|
"location": "point"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -638,7 +646,8 @@
|
||||||
"_grb_ref=feat.properties['source:geometry:entity'] + '/' + feat.properties['source:geometry:oidn']",
|
"_grb_ref=feat.properties['source:geometry:entity'] + '/' + feat.properties['source:geometry:oidn']",
|
||||||
"_imported_osm_object_found= feat.properties['_osm_obj:source:ref'] == feat.properties._grb_ref",
|
"_imported_osm_object_found= feat.properties['_osm_obj:source:ref'] == feat.properties._grb_ref",
|
||||||
"_grb_date=feat.properties['source:geometry:date'].replace(/\\//g,'-')",
|
"_grb_date=feat.properties['source:geometry:date'].replace(/\\//g,'-')",
|
||||||
"_imported_osm_still_fresh= feat.properties['_osm_obj:source:date'] == feat.properties._grb_date"
|
"_imported_osm_still_fresh= feat.properties['_osm_obj:source:date'] == feat.properties._grb_date",
|
||||||
|
"_target_building_type=feat.properties['_osm_obj:building'] === 'yes' ? feat.properties.building : (feat.properties['_osm_obj:building'] ?? feat.properties.building)"
|
||||||
],
|
],
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
{
|
{
|
||||||
|
@ -676,7 +685,7 @@
|
||||||
"mappings": [
|
"mappings": [
|
||||||
{
|
{
|
||||||
"if": "_overlaps_with!=null",
|
"if": "_overlaps_with!=null",
|
||||||
"then": "{import_button(OSM-buildings,building=$building; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Replace the geometry in OpenStreetMap,,,_osm_obj:id)}"
|
"then": "{import_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)}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue