forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
94f66eafc1
56 changed files with 2336 additions and 832 deletions
|
@ -39,20 +39,6 @@ export default class SelectedFeatureHandler {
|
|||
hash.addCallback(() => self.setSelectedElementFromHash())
|
||||
|
||||
|
||||
// IF the selected element changes, set the hash correctly
|
||||
state.selectedElement.addCallback(feature => {
|
||||
if (feature === undefined) {
|
||||
if (!SelectedFeatureHandler._no_trigger_on.has(hash.data)) {
|
||||
hash.setData("")
|
||||
}
|
||||
}
|
||||
|
||||
const h = feature?.properties?.id;
|
||||
if (h !== undefined) {
|
||||
hash.setData(h)
|
||||
}
|
||||
})
|
||||
|
||||
state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD(_ => {
|
||||
// New data was loaded. In initial startup, the hash might be set (via the URL) but might not be selected yet
|
||||
if (hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)) {
|
||||
|
|
|
@ -92,7 +92,7 @@ export default class FeaturePipeline {
|
|||
if (location?.zoom === undefined) {
|
||||
return false;
|
||||
}
|
||||
let minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom ?? 18));
|
||||
let minzoom = Math.min(...state.filteredLayers.data.map(layer => layer.layerDef.minzoom ?? 18));
|
||||
return location.zoom >= minzoom;
|
||||
}
|
||||
);
|
||||
|
@ -312,7 +312,7 @@ export default class FeaturePipeline {
|
|||
|
||||
|
||||
// Whenever fresh data comes in, we need to update the metatagging
|
||||
self.newDataLoadedSignal.stabilized(250).addCallback(src => {
|
||||
self.newDataLoadedSignal.stabilized(250).addCallback(_ => {
|
||||
self.updateAllMetaTagging()
|
||||
})
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
|||
import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject";
|
||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
|
||||
|
||||
export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> {
|
||||
|
@ -10,6 +11,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
|||
private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void;
|
||||
private readonly layer: FilteredLayer
|
||||
private readonly nodeByIds = new Map<number, OsmNode>();
|
||||
private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>()
|
||||
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
|
@ -35,7 +37,6 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
|||
this.nodeByIds.set(osmNode.id, osmNode)
|
||||
}
|
||||
|
||||
const parentWaysByNodeId = new Map<number, OsmWay[]>()
|
||||
for (const osmObj of allObjects) {
|
||||
if (osmObj.type !== "way") {
|
||||
continue
|
||||
|
@ -43,16 +44,20 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
|||
const osmWay = <OsmWay>osmObj;
|
||||
for (const nodeId of osmWay.nodes) {
|
||||
|
||||
if (!parentWaysByNodeId.has(nodeId)) {
|
||||
parentWaysByNodeId.set(nodeId, [])
|
||||
if (!this.parentWays.has(nodeId)) {
|
||||
const src = new UIEventSource<OsmWay[]>([])
|
||||
this.parentWays.set(nodeId,src)
|
||||
src.addCallback(parentWays => {
|
||||
const tgs = nodesById.get(nodeId).tags
|
||||
tgs ["parent_ways"] = JSON.stringify(parentWays.map(w => w.tags))
|
||||
tgs["parent_way_ids"] = JSON.stringify(parentWays.map(w => w.id))
|
||||
})
|
||||
}
|
||||
parentWaysByNodeId.get(nodeId).push(osmWay)
|
||||
const src = this.parentWays.get(nodeId)
|
||||
src.data.push(osmWay)
|
||||
src.ping();
|
||||
}
|
||||
}
|
||||
parentWaysByNodeId.forEach((allWays, nodeId) => {
|
||||
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 asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({
|
||||
feature: osmNode.asGeoJson(), freshness: now
|
||||
|
@ -71,10 +76,18 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
|||
* @param id
|
||||
* @constructor
|
||||
*/
|
||||
public GetNode(id: number) : OsmNode {
|
||||
public GetNode(id: number): OsmNode {
|
||||
return this.nodeByIds.get(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent way list
|
||||
* @param nodeId
|
||||
* @constructor
|
||||
*/
|
||||
public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> {
|
||||
return this.parentWays.get(nodeId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -201,9 +201,10 @@ export interface TiledFeatureSourceOptions {
|
|||
readonly minZoomLevel?: number,
|
||||
/**
|
||||
* IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated.
|
||||
* Setting 'dontEnforceMinZoomLevel' will still allow bigger zoom levels for those features
|
||||
* Setting 'dontEnforceMinZoomLevel' will still allow bigger zoom levels for those features.
|
||||
* If 'pick_first' is set, the feature will not be duplicated but set to some tile
|
||||
*/
|
||||
readonly dontEnforceMinZoom?: boolean,
|
||||
readonly dontEnforceMinZoom?: boolean | "pick_first",
|
||||
readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void,
|
||||
readonly layer?: FilteredLayer
|
||||
}
|
|
@ -25,7 +25,7 @@ export class GeoOperations {
|
|||
}
|
||||
|
||||
static centerpointCoordinates(feature: any): [number, number] {
|
||||
return <[number, number]> turf.center(feature).geometry.coordinates;
|
||||
return <[number, number]>turf.center(feature).geometry.coordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -37,10 +37,10 @@ export class GeoOperations {
|
|||
return turf.distance(lonlat0, lonlat1, {units: "meters"})
|
||||
}
|
||||
|
||||
static convexHull(featureCollection, options: {concavity?: number}){
|
||||
static convexHull(featureCollection, options: { concavity?: number }) {
|
||||
return turf.convex(featureCollection, options)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the overlap of 'feature' with every other specified feature.
|
||||
* The features with which 'feature' overlaps, are returned together with their overlap area in m²
|
||||
|
@ -199,8 +199,8 @@ export class GeoOperations {
|
|||
|
||||
static buffer(feature: any, bufferSizeInMeter: number) {
|
||||
return turf.buffer(feature, bufferSizeInMeter / 1000, {
|
||||
units:'kilometers'
|
||||
} )
|
||||
units: 'kilometers'
|
||||
})
|
||||
}
|
||||
|
||||
static bbox(feature: any) {
|
||||
|
@ -350,263 +350,166 @@ export class GeoOperations {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the intersection between two features.
|
||||
* Returns the length if intersecting a linestring and a (multi)polygon (in meters), returns a surface area (in m²) if intersecting two (multi)polygons
|
||||
* Returns 0 if both are linestrings
|
||||
* Returns null if the features are not intersecting
|
||||
*/
|
||||
private static calculateInstersection(feature, otherFeature, featureBBox: BBox, otherFeatureBBox?: BBox): number {
|
||||
if (feature.geometry.type === "LineString") {
|
||||
|
||||
|
||||
otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature);
|
||||
const overlaps = featureBBox.overlapsWith(otherFeatureBBox)
|
||||
if (!overlaps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate the length of the intersection
|
||||
|
||||
|
||||
let intersectionPoints = turf.lineIntersect(feature, otherFeature);
|
||||
if (intersectionPoints.features.length == 0) {
|
||||
// No intersections.
|
||||
// If one point is inside of the polygon, all points are
|
||||
|
||||
|
||||
const coors = feature.geometry.coordinates;
|
||||
const startCoor = coors[0]
|
||||
if (this.inside(startCoor, otherFeature)) {
|
||||
return this.lengthInMeters(feature)
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
let intersectionPointsArray = intersectionPoints.features.map(d => {
|
||||
return d.geometry.coordinates
|
||||
});
|
||||
|
||||
if (otherFeature.geometry.type === "LineString") {
|
||||
if (intersectionPointsArray.length > 0) {
|
||||
return 0
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (intersectionPointsArray.length == 1) {
|
||||
// We need to add the start- or endpoint of the current feature, depending on which one is embedded
|
||||
const coors = feature.geometry.coordinates;
|
||||
const startCoor = coors[0]
|
||||
if (this.inside(startCoor, otherFeature)) {
|
||||
// The startpoint is embedded
|
||||
intersectionPointsArray.push(startCoor)
|
||||
} else {
|
||||
intersectionPointsArray.push(coors[coors.length - 1])
|
||||
}
|
||||
}
|
||||
|
||||
let intersection = turf.lineSlice(turf.point(intersectionPointsArray[0]), turf.point(intersectionPointsArray[1]), feature);
|
||||
|
||||
if (intersection == null) {
|
||||
return null;
|
||||
}
|
||||
const intersectionSize = turf.length(intersection); // in km
|
||||
return intersectionSize * 1000
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") {
|
||||
const otherFeatureBBox = BBox.get(otherFeature);
|
||||
const overlaps = featureBBox.overlapsWith(otherFeatureBBox)
|
||||
if (!overlaps) {
|
||||
return null;
|
||||
}
|
||||
if (otherFeature.geometry.type === "LineString") {
|
||||
return this.calculateInstersection(otherFeature, feature, otherFeatureBBox, featureBBox)
|
||||
}
|
||||
|
||||
try{
|
||||
|
||||
const intersection = turf.intersect(feature, otherFeature);
|
||||
if (intersection == null) {
|
||||
return null;
|
||||
}
|
||||
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"
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates line intersection between two features.
|
||||
*/
|
||||
public static LineIntersections(feature, otherFeature): [number,number][]{
|
||||
return turf.lineIntersect(feature, otherFeature).features.map(p =><[number,number]> p.geometry.coordinates)
|
||||
public static LineIntersections(feature, otherFeature): [number, number][] {
|
||||
return turf.lineIntersect(feature, otherFeature).features.map(p => <[number, number]>p.geometry.coordinates)
|
||||
}
|
||||
|
||||
public static AsGpx(feature, generatedWithLayer?: LayerConfig){
|
||||
|
||||
|
||||
public static AsGpx(feature, generatedWithLayer?: LayerConfig) {
|
||||
|
||||
const metadata = {}
|
||||
const tags = feature.properties
|
||||
|
||||
if(generatedWithLayer !== undefined){
|
||||
|
||||
|
||||
if (generatedWithLayer !== undefined) {
|
||||
|
||||
metadata["name"] = generatedWithLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt
|
||||
metadata["desc"] = "Generated with MapComplete layer "+generatedWithLayer.id
|
||||
if(tags._backend?.contains("openstreetmap")){
|
||||
metadata["copyright"]= "Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright"
|
||||
metadata["desc"] = "Generated with MapComplete layer " + generatedWithLayer.id
|
||||
if (tags._backend?.contains("openstreetmap")) {
|
||||
metadata["copyright"] = "Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright"
|
||||
metadata["author"] = tags["_last_edit:contributor"]
|
||||
metadata["link"]= "https://www.openstreetmap.org/"+tags.id
|
||||
metadata["link"] = "https://www.openstreetmap.org/" + tags.id
|
||||
metadata["time"] = tags["_last_edit:timestamp"]
|
||||
}else{
|
||||
} else {
|
||||
metadata["time"] = new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return togpx(feature, {
|
||||
creator: "MapComplete "+Constants.vNumber,
|
||||
creator: "MapComplete " + Constants.vNumber,
|
||||
metadata
|
||||
})
|
||||
}
|
||||
|
||||
public static IdentifieCommonSegments(coordinatess: [number,number][][] ): {
|
||||
|
||||
public static IdentifieCommonSegments(coordinatess: [number, number][][]): {
|
||||
originalIndex: number,
|
||||
segmentShardWith: number[],
|
||||
coordinates: []
|
||||
}[]{
|
||||
|
||||
}[] {
|
||||
|
||||
// An edge. Note that the edge might be reversed to fix the sorting condition: start[0] < end[0] && (start[0] != end[0] || start[0] < end[1])
|
||||
type edge = {start: [number, number], end: [number, number], intermediate: [number,number][], members: {index:number, isReversed: boolean}[]}
|
||||
type edge = { start: [number, number], end: [number, number], intermediate: [number, number][], members: { index: number, isReversed: boolean }[] }
|
||||
|
||||
// The strategy:
|
||||
// 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them
|
||||
// 2. Join these edges back together - as long as their membership groups are the same
|
||||
// 3. Convert to results
|
||||
|
||||
|
||||
const allEdgesByKey = new Map<string, edge>()
|
||||
|
||||
for (let index = 0; index < coordinatess.length; index++){
|
||||
for (let index = 0; index < coordinatess.length; index++) {
|
||||
const coordinates = coordinatess[index];
|
||||
for (let i = 0; i < coordinates.length - 1; i++){
|
||||
|
||||
for (let i = 0; i < coordinates.length - 1; i++) {
|
||||
|
||||
const c0 = coordinates[i];
|
||||
const c1 = coordinates[i + 1]
|
||||
const isReversed = (c0[0] > c1[0]) || (c0[0] == c1[0] && c0[1] > c1[1])
|
||||
|
||||
let key : string
|
||||
if(isReversed){
|
||||
key = ""+c1+";"+c0
|
||||
}else{
|
||||
key = ""+c0+";"+c1
|
||||
|
||||
let key: string
|
||||
if (isReversed) {
|
||||
key = "" + c1 + ";" + c0
|
||||
} else {
|
||||
key = "" + c0 + ";" + c1
|
||||
}
|
||||
const member = {index, isReversed}
|
||||
if(allEdgesByKey.has(key)){
|
||||
if (allEdgesByKey.has(key)) {
|
||||
allEdgesByKey.get(key).members.push(member)
|
||||
continue
|
||||
}
|
||||
|
||||
let edge : edge;
|
||||
if(!isReversed){
|
||||
|
||||
let edge: edge;
|
||||
if (!isReversed) {
|
||||
edge = {
|
||||
start : c0,
|
||||
start: c0,
|
||||
end: c1,
|
||||
members: [member],
|
||||
intermediate: []
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
edge = {
|
||||
start : c1,
|
||||
start: c1,
|
||||
end: c0,
|
||||
members: [member],
|
||||
intermediate: []
|
||||
}
|
||||
}
|
||||
allEdgesByKey.set(key, edge)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Lets merge them back together!
|
||||
|
||||
|
||||
let didMergeSomething = false;
|
||||
let allMergedEdges = Array.from(allEdgesByKey.values())
|
||||
const allEdgesByStartPoint = new Map<string, edge[]>()
|
||||
for (const edge of allMergedEdges) {
|
||||
|
||||
|
||||
edge.members.sort((m0, m1) => m0.index - m1.index)
|
||||
|
||||
const kstart = edge.start+""
|
||||
if(!allEdgesByStartPoint.has(kstart)){
|
||||
|
||||
const kstart = edge.start + ""
|
||||
if (!allEdgesByStartPoint.has(kstart)) {
|
||||
allEdgesByStartPoint.set(kstart, [])
|
||||
}
|
||||
allEdgesByStartPoint.get(kstart).push(edge)
|
||||
}
|
||||
|
||||
|
||||
function membersAreCompatible(first:edge, second:edge): boolean{
|
||||
|
||||
|
||||
function membersAreCompatible(first: edge, second: edge): boolean {
|
||||
// There must be an exact match between the members
|
||||
if(first.members === second.members){
|
||||
if (first.members === second.members) {
|
||||
return true
|
||||
}
|
||||
|
||||
if(first.members.length !== second.members.length){
|
||||
|
||||
if (first.members.length !== second.members.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Members are sorted and have the same length, so we can check quickly
|
||||
for (let i = 0; i < first.members.length; i++) {
|
||||
const m0 = first.members[i]
|
||||
const m1 = second.members[i]
|
||||
if(m0.index !== m1.index || m0.isReversed !== m1.isReversed){
|
||||
if (m0.index !== m1.index || m0.isReversed !== m1.isReversed) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Allrigth, they are the same, lets mark this permanently
|
||||
second.members = first.members
|
||||
return true
|
||||
|
||||
|
||||
}
|
||||
|
||||
do{
|
||||
|
||||
do {
|
||||
didMergeSomething = false
|
||||
// We use 'allMergedEdges' as our running list
|
||||
const consumed = new Set<edge>()
|
||||
for (const edge of allMergedEdges) {
|
||||
// Can we make this edge longer at the end?
|
||||
if(consumed.has(edge)){
|
||||
if (consumed.has(edge)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
console.log("Considering edge", edge)
|
||||
const matchingEndEdges = allEdgesByStartPoint.get(edge.end+"")
|
||||
const matchingEndEdges = allEdgesByStartPoint.get(edge.end + "")
|
||||
console.log("Matchign endpoints:", matchingEndEdges)
|
||||
if(matchingEndEdges === undefined){
|
||||
if (matchingEndEdges === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
for (let i = 0; i < matchingEndEdges.length; i++){
|
||||
|
||||
|
||||
for (let i = 0; i < matchingEndEdges.length; i++) {
|
||||
const endEdge = matchingEndEdges[i];
|
||||
|
||||
if(consumed.has(endEdge)){
|
||||
|
||||
if (consumed.has(endEdge)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if(!membersAreCompatible(edge, endEdge)){
|
||||
|
||||
if (!membersAreCompatible(edge, endEdge)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// We can make the segment longer!
|
||||
didMergeSomething = true
|
||||
console.log("Merging ", edge, "with ", endEdge)
|
||||
|
@ -617,13 +520,169 @@ export class GeoOperations {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
allMergedEdges = allMergedEdges.filter(edge => !consumed.has(edge));
|
||||
|
||||
}while(didMergeSomething)
|
||||
|
||||
|
||||
} while (didMergeSomething)
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes points that do not contribute to the geometry from linestrings and the outer ring of polygons.
|
||||
* Returs a new copy of the feature
|
||||
* @param feature
|
||||
*/
|
||||
static removeOvernoding(feature: any) {
|
||||
if (feature.geometry.type !== "LineString" && feature.geometry.type !== "Polygon") {
|
||||
throw "Overnode removal is only supported on linestrings and polygons"
|
||||
}
|
||||
|
||||
const copy = {
|
||||
...feature,
|
||||
geometry: {...feature.geometry}
|
||||
}
|
||||
let coordinates: [number, number][]
|
||||
if (feature.geometry.type === "LineString") {
|
||||
coordinates = [...feature.geometry.coordinates]
|
||||
copy.geometry.coordinates = coordinates
|
||||
} else {
|
||||
coordinates = [...feature.geometry.coordinates[0]]
|
||||
copy.geometry.coordinates[0] = coordinates
|
||||
}
|
||||
|
||||
// inline replacement in the coordinates list
|
||||
for (let i = coordinates.length - 2; i >= 1; i--) {
|
||||
const coordinate = coordinates[i];
|
||||
const nextCoordinate = coordinates[i + 1]
|
||||
const prevCoordinate = coordinates[i - 1]
|
||||
|
||||
const distP = GeoOperations.distanceBetween(coordinate, prevCoordinate)
|
||||
if(distP < 0.1){
|
||||
coordinates.splice(i, 1)
|
||||
continue
|
||||
}
|
||||
|
||||
if(i == coordinates.length - 2){
|
||||
const distN = GeoOperations.distanceBetween(coordinate, nextCoordinate)
|
||||
if(distN < 0.1){
|
||||
coordinates.splice(i, 1)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const bearingN = turf.bearing(coordinate, nextCoordinate)
|
||||
const bearingP = turf.bearing(prevCoordinate, coordinate)
|
||||
const diff = Math.abs(bearingN - bearingP)
|
||||
if (diff < 4) {
|
||||
// If the diff is low, this point is hardly relevant
|
||||
coordinates.splice(i, 1)
|
||||
} else if (360 - diff < 4) {
|
||||
// In case that the line is going south, e.g. bearingN = 179, bearingP = -179
|
||||
coordinates.splice(i, 1)
|
||||
}
|
||||
|
||||
}
|
||||
return copy;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the intersection between two features.
|
||||
* Returns the length if intersecting a linestring and a (multi)polygon (in meters), returns a surface area (in m²) if intersecting two (multi)polygons
|
||||
* Returns 0 if both are linestrings
|
||||
* Returns null if the features are not intersecting
|
||||
*/
|
||||
private static calculateInstersection(feature, otherFeature, featureBBox: BBox, otherFeatureBBox?: BBox): number {
|
||||
if (feature.geometry.type === "LineString") {
|
||||
|
||||
|
||||
otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature);
|
||||
const overlaps = featureBBox.overlapsWith(otherFeatureBBox)
|
||||
if (!overlaps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate the length of the intersection
|
||||
|
||||
|
||||
let intersectionPoints = turf.lineIntersect(feature, otherFeature);
|
||||
if (intersectionPoints.features.length == 0) {
|
||||
// No intersections.
|
||||
// If one point is inside of the polygon, all points are
|
||||
|
||||
|
||||
const coors = feature.geometry.coordinates;
|
||||
const startCoor = coors[0]
|
||||
if (this.inside(startCoor, otherFeature)) {
|
||||
return this.lengthInMeters(feature)
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
let intersectionPointsArray = intersectionPoints.features.map(d => {
|
||||
return d.geometry.coordinates
|
||||
});
|
||||
|
||||
if (otherFeature.geometry.type === "LineString") {
|
||||
if (intersectionPointsArray.length > 0) {
|
||||
return 0
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (intersectionPointsArray.length == 1) {
|
||||
// We need to add the start- or endpoint of the current feature, depending on which one is embedded
|
||||
const coors = feature.geometry.coordinates;
|
||||
const startCoor = coors[0]
|
||||
if (this.inside(startCoor, otherFeature)) {
|
||||
// The startpoint is embedded
|
||||
intersectionPointsArray.push(startCoor)
|
||||
} else {
|
||||
intersectionPointsArray.push(coors[coors.length - 1])
|
||||
}
|
||||
}
|
||||
|
||||
let intersection = turf.lineSlice(turf.point(intersectionPointsArray[0]), turf.point(intersectionPointsArray[1]), feature);
|
||||
|
||||
if (intersection == null) {
|
||||
return null;
|
||||
}
|
||||
const intersectionSize = turf.length(intersection); // in km
|
||||
return intersectionSize * 1000
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") {
|
||||
const otherFeatureBBox = BBox.get(otherFeature);
|
||||
const overlaps = featureBBox.overlapsWith(otherFeatureBBox)
|
||||
if (!overlaps) {
|
||||
return null;
|
||||
}
|
||||
if (otherFeature.geometry.type === "LineString") {
|
||||
return this.calculateInstersection(otherFeature, feature, otherFeatureBBox, featureBBox)
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const intersection = turf.intersect(feature, otherFeature);
|
||||
if (intersection == null) {
|
||||
return null;
|
||||
}
|
||||
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"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ export default class MetaTagging {
|
|||
const feature = ff.feature
|
||||
const freshness = ff.freshness
|
||||
let somethingChanged = false
|
||||
let definedTags = new Set(Object.getOwnPropertyNames( feature.properties ))
|
||||
for (const metatag of metatagsToApply) {
|
||||
try {
|
||||
if (!metatag.keys.some(key => feature.properties[key] === undefined)) {
|
||||
|
@ -64,8 +65,11 @@ export default class MetaTagging {
|
|||
}
|
||||
|
||||
if (metatag.isLazy) {
|
||||
if(!metatag.keys.some(key => !definedTags.has(key))) {
|
||||
// All keys are defined - lets skip!
|
||||
continue
|
||||
}
|
||||
somethingChanged = true;
|
||||
|
||||
metatag.applyMetaTagsOnFeature(feature, freshness, layer, state)
|
||||
} else {
|
||||
const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness, layer, state)
|
||||
|
@ -84,12 +88,13 @@ export default class MetaTagging {
|
|||
}
|
||||
|
||||
if (layerFuncs !== undefined) {
|
||||
let retaggingChanged = false;
|
||||
try {
|
||||
layerFuncs(params, feature)
|
||||
retaggingChanged = layerFuncs(params, feature)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
somethingChanged = true
|
||||
somethingChanged = somethingChanged || retaggingChanged
|
||||
}
|
||||
|
||||
if (somethingChanged) {
|
||||
|
@ -99,8 +104,8 @@ export default class MetaTagging {
|
|||
}
|
||||
return atLeastOneFeatureChanged
|
||||
}
|
||||
public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] {
|
||||
const functions: ((feature: any) => void)[] = [];
|
||||
public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => boolean)[] {
|
||||
const functions: ((feature: any) => boolean)[] = [];
|
||||
|
||||
for (const entry of calculatedTags) {
|
||||
const key = entry[0]
|
||||
|
@ -110,10 +115,9 @@ export default class MetaTagging {
|
|||
continue;
|
||||
}
|
||||
|
||||
const calculateAndAssign = (feat) => {
|
||||
|
||||
|
||||
const calculateAndAssign: ((feat: any) => boolean) = (feat) => {
|
||||
try {
|
||||
let oldValue = isStrict ? feat.properties[key] : undefined
|
||||
let result = new Function("feat", "return " + code + ";")(feat);
|
||||
if (result === "") {
|
||||
result === undefined
|
||||
|
@ -124,7 +128,7 @@ export default class MetaTagging {
|
|||
}
|
||||
delete feat.properties[key]
|
||||
feat.properties[key] = result;
|
||||
return result;
|
||||
return result === oldValue;
|
||||
}catch(e){
|
||||
if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) {
|
||||
console.warn("Could not calculate a " + (isStrict ? "strict " : "") + " calculated tag for key " + key + " defined by " + code + " (in layer" + layerId + ") due to \n" + e + "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e, e.stack)
|
||||
|
@ -133,6 +137,7 @@ export default class MetaTagging {
|
|||
console.error("Got ", MetaTagging.stopErrorOutputAt, " errors calculating this metatagging - stopping output now")
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,9 +154,11 @@ export default class MetaTagging {
|
|||
configurable: true,
|
||||
enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this
|
||||
get: function () {
|
||||
return calculateAndAssign(feature)
|
||||
calculateAndAssign(feature)
|
||||
return feature.properties[key]
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
|
@ -160,17 +167,23 @@ export default class MetaTagging {
|
|||
return functions;
|
||||
}
|
||||
|
||||
private static retaggingFuncCache = new Map<string, ((feature: any) => void)[]>()
|
||||
private static retaggingFuncCache = new Map<string, ((feature: any) => boolean)[]>()
|
||||
|
||||
/**
|
||||
* Creates the function which adds all the calculated tags to a feature. Called once per layer
|
||||
* @param layer
|
||||
* @param state
|
||||
* @private
|
||||
*/
|
||||
private static createRetaggingFunc(layer: LayerConfig, state):
|
||||
((params: ExtraFuncParams, feature: any) => void) {
|
||||
((params: ExtraFuncParams, feature: any) => boolean) {
|
||||
|
||||
const calculatedTags: [string, string, boolean][] = layer.calculatedTags;
|
||||
if (calculatedTags === undefined || calculatedTags.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let functions = MetaTagging.retaggingFuncCache.get(layer.id);
|
||||
let functions :((feature: any) => boolean)[] = MetaTagging.retaggingFuncCache.get(layer.id);
|
||||
if (functions === undefined) {
|
||||
functions = MetaTagging.createFunctionsForFeature(layer.id, calculatedTags)
|
||||
MetaTagging.retaggingFuncCache.set(layer.id, functions)
|
||||
|
@ -192,6 +205,7 @@ export default class MetaTagging {
|
|||
} catch (e) {
|
||||
console.error("Invalid syntax in calculated tags or some other error: ", e)
|
||||
}
|
||||
return true; // Something changed
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -81,8 +81,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public async getPreview(): Promise<FeatureSource> {
|
||||
const {closestIds, allNodesById, detachedNodeIds} = await this.GetClosestIds();
|
||||
console.debug("Generating preview, identicals are ",)
|
||||
const {closestIds, allNodesById, detachedNodes, reprojectedNodes} = await this.GetClosestIds();
|
||||
const preview: GeoJSONObject[] = closestIds.map((newId, i) => {
|
||||
if (this.identicalTo[i] !== undefined) {
|
||||
return undefined
|
||||
|
@ -93,7 +92,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
type: "Feature",
|
||||
properties: {
|
||||
"newpoint": "yes",
|
||||
"id": "replace-geometry-move-" + i
|
||||
"id": "replace-geometry-move-" + i,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
|
@ -101,49 +100,316 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
}
|
||||
};
|
||||
}
|
||||
const origPoint = allNodesById.get(newId).centerpoint()
|
||||
|
||||
const origNode = allNodesById.get(newId);
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"move": "yes",
|
||||
"osm-id": newId,
|
||||
"id": "replace-geometry-move-" + i
|
||||
"id": "replace-geometry-move-" + i,
|
||||
"original-node-tags": JSON.stringify(origNode.tags)
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [[origPoint[1], origPoint[0]], this.targetCoordinates[i]]
|
||||
coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]]
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
for (const detachedNodeId of detachedNodeIds) {
|
||||
const origPoint = allNodesById.get(detachedNodeId).centerpoint()
|
||||
|
||||
reprojectedNodes.forEach(({newLat, newLon, nodeId}) => {
|
||||
|
||||
const origNode = allNodesById.get(nodeId);
|
||||
const feature = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"move": "yes",
|
||||
"reprojection": "yes",
|
||||
"osm-id": nodeId,
|
||||
"id": "replace-geometry-reproject-" + nodeId,
|
||||
"original-node-tags": JSON.stringify(origNode.tags)
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [[origNode.lon, origNode.lat], [newLon, newLat]]
|
||||
}
|
||||
};
|
||||
preview.push(feature)
|
||||
})
|
||||
|
||||
|
||||
detachedNodes.forEach(({reason}, id) => {
|
||||
const origNode = allNodesById.get(id);
|
||||
const feature = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"detach": "yes",
|
||||
"id": "replace-geometry-detach-" + detachedNodeId
|
||||
"id": "replace-geometry-detach-" + id,
|
||||
"detach-reason": reason,
|
||||
"original-node-tags": JSON.stringify(origNode.tags)
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [origPoint[1], origPoint[0]]
|
||||
coordinates: [origNode.lon, origNode.lat]
|
||||
}
|
||||
};
|
||||
preview.push(feature)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return new StaticFeatureSource(Utils.NoNull(preview), false)
|
||||
|
||||
}
|
||||
|
||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
/**
|
||||
* For 'this.feature`, gets a corresponding closest node that alreay exsists.
|
||||
*
|
||||
* This method contains the main logic for this module, as it decides which node gets moved where.
|
||||
*
|
||||
*/
|
||||
public 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,
|
||||
detachedNodes: Map<number, {
|
||||
reason: string,
|
||||
hasTags: boolean
|
||||
}>,
|
||||
reprojectedNodes: Map<number, {
|
||||
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
||||
projectAfterIndex: number,
|
||||
distance: number,
|
||||
newLat: number,
|
||||
newLon: number,
|
||||
nodeId: number
|
||||
}>
|
||||
}> {
|
||||
// TODO FIXME: if a new point has to be created, snap to already existing ways
|
||||
|
||||
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)"
|
||||
}
|
||||
const self = this;
|
||||
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 ?? "https://openstreetmap.org"}/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")
|
||||
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)
|
||||
}
|
||||
/**
|
||||
* For every already existing OSM-point, we calculate:
|
||||
*
|
||||
* - the distance to every target point.
|
||||
* - Wether this node has (other) parent ways, which might restrict movement
|
||||
* - Wether this node has tags set
|
||||
*
|
||||
* Having tags and/or being connected to another way indicate that there is some _relation_ with objects in the neighbourhood.
|
||||
*
|
||||
* The Replace-geometry action should try its best to honour these. Some 'wiggling' is allowed (e.g. moving an entrance a bit), but these relations should not be broken.l
|
||||
*/
|
||||
const distances = new Map<number /* osmId*/,
|
||||
/** target coordinate index --> distance (or undefined if a duplicate)*/
|
||||
number[]>();
|
||||
|
||||
const nodeInfo = new Map<number /* osmId*/, {
|
||||
distances: number[],
|
||||
// Part of some other way then the one that should be replaced
|
||||
partOfWay: boolean,
|
||||
hasTags: boolean
|
||||
}>()
|
||||
|
||||
for (const node of allNodes) {
|
||||
|
||||
const parentWays = nodeDb.GetParentWays(node.id)
|
||||
if (parentWays === undefined) {
|
||||
throw "PANIC: the way to replace has a node which has no parents at all. Is it deleted in the meantime?"
|
||||
}
|
||||
const parentWayIds = parentWays.data.map(w => w.type + "/" + w.id)
|
||||
const idIndex = parentWayIds.indexOf(this.wayToReplaceId)
|
||||
if (idIndex < 0) {
|
||||
throw "PANIC: the way to replace has a node, which is _not_ part of this was according to the node..."
|
||||
}
|
||||
parentWayIds.splice(idIndex, 1)
|
||||
const partOfSomeWay = parentWayIds.length > 0
|
||||
const hasTags = Object.keys(node.tags).length > 1;
|
||||
|
||||
const nodeDistances = this.targetCoordinates.map(_ => undefined)
|
||||
for (let i = 0; i < this.targetCoordinates.length; i++) {
|
||||
if (this.identicalTo[i] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
const targetCoordinate = this.targetCoordinates[i];
|
||||
const cp = node.centerpoint()
|
||||
const d = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]])
|
||||
if (d > 25) {
|
||||
// This is too much to move
|
||||
continue
|
||||
}
|
||||
if (d < 3 || !(hasTags || partOfSomeWay)) {
|
||||
// If there is some relation: cap the move distance to 3m
|
||||
nodeDistances[i] = d;
|
||||
}
|
||||
|
||||
}
|
||||
distances.set(node.id, nodeDistances)
|
||||
nodeInfo.set(node.id, {
|
||||
distances: nodeDistances,
|
||||
partOfWay: partOfSomeWay,
|
||||
hasTags
|
||||
})
|
||||
}
|
||||
|
||||
const closestIds = this.targetCoordinates.map(_ => undefined)
|
||||
const unusedIds = new Map<number, {
|
||||
reason: string,
|
||||
hasTags: boolean
|
||||
}>();
|
||||
{
|
||||
// Search best merge candidate
|
||||
/**
|
||||
* Then, we search the node that has to move the least distance and add this as mapping.
|
||||
* We do this until no points are left
|
||||
*/
|
||||
let candidate: number;
|
||||
let moveDistance: number;
|
||||
/**
|
||||
* The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates
|
||||
*/
|
||||
do {
|
||||
candidate = undefined;
|
||||
moveDistance = Infinity;
|
||||
distances.forEach((distances, nodeId) => {
|
||||
const minDist = Math.min(...Utils.NoNull(distances))
|
||||
if (moveDistance > minDist) {
|
||||
// We have found a candidate to move
|
||||
candidate = nodeId
|
||||
moveDistance = minDist
|
||||
}
|
||||
})
|
||||
|
||||
if (candidate !== undefined) {
|
||||
// We found a candidate... Search the corresponding target id:
|
||||
let targetId: number = undefined;
|
||||
let lowestDistance = Number.MAX_VALUE
|
||||
let nodeDistances = distances.get(candidate)
|
||||
for (let i = 0; i < nodeDistances.length; i++) {
|
||||
const d = nodeDistances[i]
|
||||
if (d !== undefined && d < lowestDistance) {
|
||||
lowestDistance = d;
|
||||
targetId = i;
|
||||
}
|
||||
}
|
||||
|
||||
// This candidates role is done, it can be removed from the distance matrix
|
||||
distances.delete(candidate)
|
||||
|
||||
if (targetId !== undefined) {
|
||||
// At this point, we have our target coordinate index: targetId!
|
||||
// Lets map it...
|
||||
closestIds[targetId] = candidate
|
||||
|
||||
// To indicate that this targetCoordinate is taken, we remove them from the distances matrix
|
||||
distances.forEach(dists => {
|
||||
dists[targetId] = undefined
|
||||
})
|
||||
} else {
|
||||
// Seems like all the targetCoordinates have found a source point
|
||||
unusedIds.set(candidate, {
|
||||
reason: "Unused by new way",
|
||||
hasTags: nodeInfo.get(candidate).hasTags
|
||||
})
|
||||
}
|
||||
}
|
||||
} while (candidate !== undefined)
|
||||
}
|
||||
|
||||
// If there are still unused values in 'distances', they are definitively unused
|
||||
distances.forEach((_, nodeId) => {
|
||||
unusedIds.set(nodeId, {
|
||||
reason: "Unused by new way",
|
||||
hasTags: nodeInfo.get(nodeId).hasTags
|
||||
})
|
||||
})
|
||||
|
||||
const reprojectedNodes = new Map<number, {
|
||||
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
||||
projectAfterIndex: number,
|
||||
distance: number,
|
||||
newLat: number,
|
||||
newLon: number,
|
||||
nodeId: number
|
||||
}>();
|
||||
{
|
||||
// Lets check the unused ids: can they be detached or do they signify some relation with the object?
|
||||
unusedIds.forEach(({}, id) => {
|
||||
const info = nodeInfo.get(id)
|
||||
if (!(info.hasTags || info.partOfWay)) {
|
||||
// Nothing special here, we detach
|
||||
return
|
||||
}
|
||||
|
||||
// The current node has tags and/or has an attached other building.
|
||||
// We should project them and move them onto the building on an appropriate place
|
||||
const node = allNodesById.get(id)
|
||||
|
||||
// Project the node onto the target way to calculate the new coordinates
|
||||
const way = {
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: self.targetCoordinates
|
||||
}
|
||||
};
|
||||
const projected = GeoOperations.nearestPoint(
|
||||
way, [node.lon, node.lat]
|
||||
)
|
||||
reprojectedNodes.set(id, {
|
||||
newLon: projected.geometry.coordinates[0],
|
||||
newLat: projected.geometry.coordinates[1],
|
||||
projectAfterIndex: projected.properties.index,
|
||||
distance: projected.properties.dist,
|
||||
nodeId: id
|
||||
})
|
||||
})
|
||||
|
||||
reprojectedNodes.forEach((_, nodeId) => unusedIds.delete(nodeId))
|
||||
|
||||
}
|
||||
|
||||
|
||||
return {closestIds, allNodesById, osmWay, detachedNodes: unusedIds, reprojectedNodes};
|
||||
}
|
||||
|
||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
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)"
|
||||
}
|
||||
|
||||
const {closestIds, osmWay, detachedNodes, reprojectedNodes} = await this.GetClosestIds()
|
||||
const allChanges: ChangeDescription[] = []
|
||||
const actualIdsToUse: number[] = []
|
||||
|
||||
const {closestIds, osmWay, detachedNodeIds} = await this.GetClosestIds()
|
||||
|
||||
for (let i = 0; i < closestIds.length; i++) {
|
||||
if (this.identicalTo[i] !== undefined) {
|
||||
const j = this.identicalTo[i]
|
||||
|
@ -193,13 +459,46 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes))
|
||||
}
|
||||
|
||||
const newCoordinates = [...this.targetCoordinates]
|
||||
|
||||
{
|
||||
// Add reprojected nodes to the way
|
||||
|
||||
const proj = Array.from(reprojectedNodes.values())
|
||||
proj.sort((a, b) => {
|
||||
// Sort descending
|
||||
const diff = b.projectAfterIndex - a.projectAfterIndex;
|
||||
if(diff !== 0){
|
||||
return diff
|
||||
}
|
||||
return b.distance - a.distance;
|
||||
|
||||
|
||||
})
|
||||
|
||||
for (const reprojectedNode of proj) {
|
||||
const change = <ChangeDescription>{
|
||||
id: reprojectedNode.nodeId,
|
||||
type: "node",
|
||||
meta: {
|
||||
theme: this.theme,
|
||||
changeType: "move"
|
||||
},
|
||||
changes: {lon: reprojectedNode.newLon, lat: reprojectedNode.newLat}
|
||||
}
|
||||
allChanges.push(change)
|
||||
actualIdsToUse.splice(reprojectedNode.projectAfterIndex + 1, 0, reprojectedNode.nodeId)
|
||||
newCoordinates.splice(reprojectedNode.projectAfterIndex + 1, 0, [reprojectedNode.newLon, reprojectedNode.newLat])
|
||||
}
|
||||
}
|
||||
|
||||
// Actually change the nodes of the way!
|
||||
allChanges.push({
|
||||
type: "way",
|
||||
id: osmWay.id,
|
||||
changes: {
|
||||
nodes: actualIdsToUse,
|
||||
coordinates: this.targetCoordinates
|
||||
coordinates: newCoordinates
|
||||
},
|
||||
meta: {
|
||||
theme: this.theme,
|
||||
|
@ -209,38 +508,38 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
|
||||
|
||||
// Some nodes might need to be deleted
|
||||
if (detachedNodeIds.length > 0) {
|
||||
if (detachedNodes.size > 0) {
|
||||
detachedNodes.forEach(({hasTags, reason}, nodeId) => {
|
||||
const parentWays = nodeDb.GetParentWays(nodeId)
|
||||
const index = parentWays.data.map(w => w.id).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)
|
||||
return;
|
||||
}
|
||||
// We detachted this node - so we unregister
|
||||
parentWays.data.splice(index, 1)
|
||||
parentWays.ping();
|
||||
|
||||
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;
|
||||
if (hasTags) {
|
||||
// Has tags: we leave this node alone
|
||||
return;
|
||||
}
|
||||
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,
|
||||
})
|
||||
|
||||
if (parentWays.data.length != 0) {
|
||||
// Still part of other ways: we leave this node alone!
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
@ -248,131 +547,5 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
return allChanges
|
||||
}
|
||||
|
||||
/**
|
||||
* For 'this.feature`, gets a corresponding closest node that alreay exsists.
|
||||
*
|
||||
* This method contains the main logic for this module, as it decides which node gets moved where.
|
||||
*
|
||||
*/
|
||||
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: if a new point has to be created, snap to already existing ways
|
||||
|
||||
|
||||
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")
|
||||
|
||||
/**
|
||||
* For every already existing OSM-point, we calculate the distance to every target point
|
||||
*/
|
||||
|
||||
const distances = new Map<number /* osmId*/, number[] /* target coordinate index --> distance (or undefined if a duplicate)*/>();
|
||||
for (const node of allNodes) {
|
||||
const nodeDistances = this.targetCoordinates.map(_ => undefined)
|
||||
for (let i = 0; i < this.targetCoordinates.length; i++) {
|
||||
if (this.identicalTo[i] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
const targetCoordinate = this.targetCoordinates[i];
|
||||
const cp = node.centerpoint()
|
||||
nodeDistances[i] = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]])
|
||||
}
|
||||
distances.set(node.id, nodeDistances)
|
||||
}
|
||||
|
||||
/**
|
||||
* Then, we search the node that has to move the least distance and add this as mapping.
|
||||
* We do this until no points are left
|
||||
*/
|
||||
let candidate: number;
|
||||
let moveDistance: number;
|
||||
const closestIds = this.targetCoordinates.map(_ => undefined)
|
||||
/**
|
||||
* The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates
|
||||
*/
|
||||
const unusedIds = []
|
||||
do {
|
||||
candidate = undefined;
|
||||
moveDistance = Infinity;
|
||||
distances.forEach((distances, nodeId) => {
|
||||
const minDist = Math.min(...Utils.NoNull(distances))
|
||||
if (moveDistance > minDist) {
|
||||
// We have found a candidate to move
|
||||
candidate = nodeId
|
||||
moveDistance = minDist
|
||||
}
|
||||
})
|
||||
|
||||
if (candidate !== undefined) {
|
||||
// We found a candidate... Search the corresponding target id:
|
||||
let targetId: number = undefined;
|
||||
let lowestDistance = Number.MAX_VALUE
|
||||
let nodeDistances = distances.get(candidate)
|
||||
for (let i = 0; i < nodeDistances.length; i++) {
|
||||
const d = nodeDistances[i]
|
||||
if (d !== undefined && d < lowestDistance) {
|
||||
lowestDistance = d;
|
||||
targetId = i;
|
||||
}
|
||||
}
|
||||
|
||||
// This candidates role is done, it can be removed from the distance matrix
|
||||
distances.delete(candidate)
|
||||
|
||||
if (targetId !== undefined) {
|
||||
// At this point, we have our target coordinate index: targetId!
|
||||
// Lets map it...
|
||||
closestIds[targetId] = candidate
|
||||
|
||||
// To indicate that this targetCoordinate is taken, we remove them from the distances matrix
|
||||
distances.forEach(dists => {
|
||||
dists[targetId] = undefined
|
||||
})
|
||||
} else {
|
||||
// Seems like all the targetCoordinates have found a source point
|
||||
unusedIds.push(candidate)
|
||||
}
|
||||
}
|
||||
} while (candidate !== undefined)
|
||||
|
||||
|
||||
// If there are still unused values in 'distances', they are definitively unused
|
||||
distances.forEach((_, nodeId) => {
|
||||
unusedIds.push(nodeId)
|
||||
})
|
||||
|
||||
{
|
||||
// Some extra data is included for rendering
|
||||
const osmWay = <OsmWay>parsed[parsed.length - 1]
|
||||
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};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -391,10 +391,9 @@ export class OsmWay extends OsmObject {
|
|||
// This is probably part of a relation which hasn't been fully downloaded
|
||||
continue;
|
||||
}
|
||||
const cp = node.centerpoint();
|
||||
this.coordinates.push(cp);
|
||||
latSum += cp[0]
|
||||
lonSum += cp[1]
|
||||
this.coordinates.push(node.centerpoint());
|
||||
latSum += node.lat
|
||||
lonSum += node.lon
|
||||
}
|
||||
let count = this.coordinates.length;
|
||||
this.lat = latSum / count;
|
||||
|
|
|
@ -272,7 +272,7 @@ export default class SimpleMetaTaggers {
|
|||
public static country = new CountryTagger()
|
||||
private static isOpen = new SimpleMetaTagger(
|
||||
{
|
||||
keys: ["_isOpen", "_isOpen:description"],
|
||||
keys: ["_isOpen"],
|
||||
doc: "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')",
|
||||
includesDates: true,
|
||||
isLazy: true
|
||||
|
@ -283,7 +283,7 @@ export default class SimpleMetaTaggers {
|
|||
// isOpen is irrelevant
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Object.defineProperty(feature.properties, "_isOpen", {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
|
@ -291,7 +291,8 @@ export default class SimpleMetaTaggers {
|
|||
delete feature.properties._isOpen
|
||||
feature.properties._isOpen = undefined
|
||||
const tagsSource = state.allElements.getEventSourceById(feature.properties.id);
|
||||
tagsSource.addCallbackAndRunD(tags => {
|
||||
tagsSource
|
||||
.addCallbackAndRunD(tags => {
|
||||
if (tags.opening_hours === undefined || tags._country === undefined) {
|
||||
return;
|
||||
}
|
||||
|
@ -341,7 +342,6 @@ export default class SimpleMetaTaggers {
|
|||
}
|
||||
}
|
||||
updateTags();
|
||||
return true; // Our job is done, lets unregister!
|
||||
} catch (e) {
|
||||
console.warn("Error while parsing opening hours of ", tags.id, e);
|
||||
delete tags._isOpen
|
||||
|
@ -352,6 +352,7 @@ export default class SimpleMetaTaggers {
|
|||
return undefined
|
||||
}
|
||||
})
|
||||
return true;
|
||||
|
||||
})
|
||||
)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import * as idb from "idb-keyval"
|
||||
import ScriptUtils from "../../scripts/ScriptUtils";
|
||||
import {Utils} from "../../Utils";
|
||||
/**
|
||||
* UIEventsource-wrapper around indexedDB key-value
|
||||
*/
|
||||
|
@ -8,6 +10,9 @@ export class IdbLocalStorage {
|
|||
|
||||
public static Get<T>(key: string, options: { defaultValue?: T }): UIEventSource<T>{
|
||||
const src = new UIEventSource<T>(options.defaultValue, "idb-local-storage:"+key)
|
||||
if(Utils.runningFromConsole){
|
||||
return src;
|
||||
}
|
||||
idb.get(key).then(v => src.setData(v ?? options.defaultValue))
|
||||
src.addCallback(v => idb.set(key, v))
|
||||
return src;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as mangrove from 'mangrove-reviews'
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {Review} from "./Review";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export class MangroveIdentity {
|
||||
public keypair: any = undefined;
|
||||
|
@ -23,7 +24,7 @@ export class MangroveIdentity {
|
|||
})
|
||||
})
|
||||
try {
|
||||
if ((mangroveIdentity.data ?? "") === "") {
|
||||
if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") {
|
||||
this.CreateIdentity();
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue