Fix various bugs
This commit is contained in:
parent
30f4be183e
commit
5284f198d8
26 changed files with 339 additions and 119 deletions
|
@ -528,7 +528,7 @@ function stackHists<K, V>(hists: [V, Histogram<K>][]): [V, Histogram<K>][] {
|
||||||
runningTotals.bumpHist(hist)
|
runningTotals.bumpHist(hist)
|
||||||
result.push([vhist[0], clone])
|
result.push([vhist[0], clone])
|
||||||
})
|
})
|
||||||
result.reverse()
|
result.reverse(/* Changes in place, safe copy*/)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -239,7 +239,7 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
|
||||||
prefered = preferedCategory.data;
|
prefered = preferedCategory.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
prefered.reverse();
|
prefered.reverse(/*New list, inplace reverse is fine*/);
|
||||||
for (const category of prefered) {
|
for (const category of prefered) {
|
||||||
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
|
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
|
||||||
available.sort((a, b) => {
|
available.sort((a, b) => {
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default class FeaturePipeline {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
const expiryInSeconds = Math.min(...state.layoutToUse.layers.map(l => l.maxAgeOfCache))
|
const expiryInSeconds = Math.min(...state.layoutToUse?.layers?.map(l => l.maxAgeOfCache) ?? [])
|
||||||
this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds);
|
this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds);
|
||||||
this.osmSourceZoomLevel = state.osmApiTileSize.data;
|
this.osmSourceZoomLevel = state.osmApiTileSize.data;
|
||||||
const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12))
|
const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12))
|
||||||
|
|
|
@ -74,7 +74,7 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
||||||
// We only apply the last change as that one'll have the latest geometry
|
// We only apply the last change as that one'll have the latest geometry
|
||||||
const change = changesForFeature[changesForFeature.length - 1]
|
const change = changesForFeature[changesForFeature.length - 1]
|
||||||
copy.feature.geometry = ChangeDescriptionTools.getGeojsonGeometry(change)
|
copy.feature.geometry = ChangeDescriptionTools.getGeojsonGeometry(change)
|
||||||
console.log("Applying a geometry change onto ", feature, change, copy)
|
console.log("Applying a geometry change onto:", feature,"The change is:", change,"which becomes:", copy)
|
||||||
newFeatures.push(copy)
|
newFeatures.push(copy)
|
||||||
}
|
}
|
||||||
this.features.setData(newFeatures)
|
this.features.setData(newFeatures)
|
||||||
|
|
|
@ -79,7 +79,7 @@ export default class OsmFeatureSource {
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const neededLayers = options.state.layoutToUse.layers
|
const neededLayers = (options.state.layoutToUse?.layers ?? [])
|
||||||
.filter(layer => !layer.doNotDownload)
|
.filter(layer => !layer.doNotDownload)
|
||||||
.filter(layer => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer)
|
.filter(layer => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer)
|
||||||
this.allowedTags = new Or(neededLayers.map(l => l.source.osmTags))
|
this.allowedTags = new Or(neededLayers.map(l => l.source.osmTags))
|
||||||
|
|
|
@ -81,7 +81,7 @@ export class ChangeDescriptionTools {
|
||||||
case "way":
|
case "way":
|
||||||
const w = new OsmWay(change.id)
|
const w = new OsmWay(change.id)
|
||||||
w.nodes = change.changes["nodes"]
|
w.nodes = change.changes["nodes"]
|
||||||
w.coordinates = change.changes["coordinates"].map(coor => coor.reverse())
|
w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [lat, lon])
|
||||||
return w.asGeoJson().geometry
|
return w.asGeoJson().geometry
|
||||||
case "relation":
|
case "relation":
|
||||||
const r = new OsmRelation(change.id)
|
const r = new OsmRelation(change.id)
|
||||||
|
|
|
@ -33,12 +33,12 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
|
||||||
super(null, true);
|
super(null, true);
|
||||||
this._tags = [...tags, new Tag("type", "multipolygon")];
|
this._tags = [...tags, new Tag("type", "multipolygon")];
|
||||||
this.changeType = changeType;
|
this.changeType = changeType;
|
||||||
this.theme = state.layoutToUse.id
|
this.theme = state?.layoutToUse?.id ?? ""
|
||||||
this.createOuterWay = new CreateWayWithPointReuseAction([], outerRingCoordinates, state, config)
|
this.createOuterWay = new CreateWayWithPointReuseAction([], outerRingCoordinates, state, config)
|
||||||
this.createInnerWays = innerRingsCoordinates.map(ringCoordinates =>
|
this.createInnerWays = innerRingsCoordinates.map(ringCoordinates =>
|
||||||
new CreateNewWayAction([],
|
new CreateNewWayAction([],
|
||||||
ringCoordinates.map(([lon, lat]) => ({lat, lon})),
|
ringCoordinates.map(([lon, lat]) => ({lat, lon})),
|
||||||
{theme: state.layoutToUse.id}))
|
{theme: state?.layoutToUse?.id}))
|
||||||
|
|
||||||
this.geojsonPreview = {
|
this.geojsonPreview = {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
|
|
|
@ -112,16 +112,25 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
||||||
|
|
||||||
const geojson = this._snapOnto.asGeoJson()
|
const geojson = this._snapOnto.asGeoJson()
|
||||||
const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat])
|
const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat])
|
||||||
|
const projectedCoor= <[number, number]>projected.geometry.coordinates
|
||||||
const index = projected.properties.index
|
const index = projected.properties.index
|
||||||
// We check that it isn't close to an already existing point
|
// We check that it isn't close to an already existing point
|
||||||
let reusedPointId = undefined;
|
let reusedPointId = undefined;
|
||||||
const prev = <[number, number]>geojson.geometry.coordinates[index]
|
let outerring : [number,number][];
|
||||||
if (GeoOperations.distanceBetween(prev, <[number, number]>projected.geometry.coordinates) < this._reusePointDistance) {
|
|
||||||
|
if(geojson.geometry.type === "LineString"){
|
||||||
|
outerring = <[number, number][]> geojson.geometry.coordinates
|
||||||
|
}else if(geojson.geometry.type === "Polygon"){
|
||||||
|
outerring =<[number, number][]> geojson.geometry.coordinates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev= outerring[index]
|
||||||
|
if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) {
|
||||||
// We reuse this point instead!
|
// We reuse this point instead!
|
||||||
reusedPointId = this._snapOnto.nodes[index]
|
reusedPointId = this._snapOnto.nodes[index]
|
||||||
}
|
}
|
||||||
const next = <[number, number]>geojson.geometry.coordinates[index + 1]
|
const next = outerring[index + 1]
|
||||||
if (GeoOperations.distanceBetween(next, <[number, number]>projected.geometry.coordinates) < this._reusePointDistance) {
|
if (GeoOperations.distanceBetween(next, projectedCoor) < this._reusePointDistance) {
|
||||||
// We reuse this point instead!
|
// We reuse this point instead!
|
||||||
reusedPointId = this._snapOnto.nodes[index + 1]
|
reusedPointId = this._snapOnto.nodes[index + 1]
|
||||||
}
|
}
|
||||||
|
@ -135,8 +144,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
const locations = [...this._snapOnto.coordinates]
|
const locations = [...this._snapOnto.coordinates.map(([lat, lon]) =><[number,number]> [lon, lat])]
|
||||||
locations.forEach(coor => coor.reverse())
|
|
||||||
const ids = [...this._snapOnto.nodes]
|
const ids = [...this._snapOnto.nodes]
|
||||||
|
|
||||||
locations.splice(index + 1, 0, [this._lon, this._lat])
|
locations.splice(index + 1, 0, [this._lon, this._lat])
|
||||||
|
|
|
@ -33,7 +33,7 @@ export default class CreateNewWayAction extends OsmCreateAction {
|
||||||
We filter those here, as the CreateWayWithPointReuseAction delegates the actual creation to here.
|
We filter those here, as the CreateWayWithPointReuseAction delegates the actual creation to here.
|
||||||
Filtering here also prevents similar bugs in other actions
|
Filtering here also prevents similar bugs in other actions
|
||||||
*/
|
*/
|
||||||
if(this.coordinates.length > 0 && this.coordinates[this.coordinates.length - 1].nodeId === coordinate.nodeId){
|
if(this.coordinates.length > 0 && coordinate.nodeId !== undefined && this.coordinates[this.coordinates.length - 1].nodeId === coordinate.nodeId){
|
||||||
// This is a duplicate id
|
// This is a duplicate id
|
||||||
console.warn("Skipping a node in createWay to avoid a duplicate node:", coordinate,"\nThe previous coordinates are: ", this.coordinates)
|
console.warn("Skipping a node in createWay to avoid a duplicate node:", coordinate,"\nThe previous coordinates are: ", this.coordinates)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -186,7 +186,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||||
const theme = this._state.layoutToUse.id
|
const theme = this._state?.layoutToUse?.id
|
||||||
const allChanges: ChangeDescription[] = []
|
const allChanges: ChangeDescription[] = []
|
||||||
const nodeIdsToUse: { lat: number, lon: number, nodeId?: number }[] = []
|
const nodeIdsToUse: { lat: number, lon: number, nodeId?: number }[] = []
|
||||||
for (let i = 0; i < this._coordinateInfo.length; i++) {
|
for (let i = 0; i < this._coordinateInfo.length; i++) {
|
||||||
|
@ -251,7 +251,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
||||||
|
|
||||||
const bbox = new BBox(coordinates)
|
const bbox = new BBox(coordinates)
|
||||||
const state = this._state
|
const state = this._state
|
||||||
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
|
// Init coordianteinfo with undefined but the same length as coordinates
|
||||||
|
|
|
@ -28,6 +28,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
/**
|
/**
|
||||||
* 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]
|
* This is identical to either this.feature.geometry.coordinates or -in case of a polygon- feature.geometry.coordinates[0]
|
||||||
|
* Format: [lon, lat]
|
||||||
*/
|
*/
|
||||||
private readonly targetCoordinates: [number, number][];
|
private readonly targetCoordinates: [number, number][];
|
||||||
/**
|
/**
|
||||||
|
@ -540,8 +541,6 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
id: nodeId,
|
id: nodeId,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return allChanges
|
return allChanges
|
||||||
|
|
|
@ -55,8 +55,8 @@ export class Changes {
|
||||||
// This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
|
// This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createChangesetFor(csId: string,
|
static createChangesetFor(csId: string,
|
||||||
allChanges: {
|
allChanges: {
|
||||||
modifiedObjects: OsmObject[],
|
modifiedObjects: OsmObject[],
|
||||||
newObjects: OsmObject[],
|
newObjects: OsmObject[],
|
||||||
deletedObjects: OsmObject[]
|
deletedObjects: OsmObject[]
|
||||||
|
|
|
@ -207,27 +207,36 @@ export abstract class OsmObject {
|
||||||
return objects;
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the list of polygon features to determine if the given tags are a polygon or not.
|
||||||
|
* */
|
||||||
protected static isPolygon(tags: any): boolean {
|
protected static isPolygon(tags: any): boolean {
|
||||||
for (const tagsKey in tags) {
|
for (const tagsKey in tags) {
|
||||||
if (!tags.hasOwnProperty(tagsKey)) {
|
if (!tags.hasOwnProperty(tagsKey)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const polyGuide = OsmObject.polygonFeatures.get(tagsKey)
|
const polyGuide : { values: Set<string>; blacklist: boolean } = OsmObject.polygonFeatures.get(tagsKey)
|
||||||
if (polyGuide === undefined) {
|
if (polyGuide === undefined) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if ((polyGuide.values === null)) {
|
if ((polyGuide.values === null)) {
|
||||||
// We match all
|
// .values is null, thus merely _having_ this key is enough to be a polygon (or if blacklist, being a line)
|
||||||
return !polyGuide.blacklist
|
return !polyGuide.blacklist
|
||||||
}
|
}
|
||||||
// is the key contained?
|
// is the key contained? Then we have a match if the value is contained
|
||||||
return polyGuide.values.has(tags[tagsKey])
|
const doesMatch = polyGuide.values.has(tags[tagsKey])
|
||||||
|
if(polyGuide.blacklist){
|
||||||
|
return !doesMatch
|
||||||
|
}
|
||||||
|
return doesMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static constructPolygonFeatures(): Map<string, { values: Set<string>, blacklist: boolean }> {
|
private static constructPolygonFeatures(): Map<string, { values: Set<string>, blacklist: boolean }> {
|
||||||
const result = new Map<string, { values: Set<string>, blacklist: boolean }>();
|
const result = new Map<string, { values: Set<string>, blacklist: boolean }>();
|
||||||
for (const polygonFeature of polygon_features) {
|
for (const polygonFeature of (polygon_features["default"] ?? polygon_features)) {
|
||||||
const key = polygonFeature.key;
|
const key = polygonFeature.key;
|
||||||
|
|
||||||
if (polygonFeature.polygon === "all") {
|
if (polygonFeature.polygon === "all") {
|
||||||
|
@ -381,7 +390,7 @@ export class OsmWay extends OsmObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.nodes === undefined) {
|
if (element.nodes === undefined) {
|
||||||
console.log("PANIC")
|
console.error("PANIC: no nodes!")
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const nodeId of element.nodes) {
|
for (const nodeId of element.nodes) {
|
||||||
|
@ -417,7 +426,9 @@ export class OsmWay extends OsmObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private isPolygon(): boolean {
|
private isPolygon(): boolean {
|
||||||
if (this.coordinates[0] !== this.coordinates[this.coordinates.length - 1]) {
|
// Compare lat and lon seperately, as the coordinate array might not be a reference to the same object
|
||||||
|
if (this.coordinates[0][0] !== this.coordinates[this.coordinates.length - 1][0] ||
|
||||||
|
this.coordinates[0][1] !== this.coordinates[this.coordinates.length - 1][1] ) {
|
||||||
return false; // Not closed
|
return false; // Not closed
|
||||||
}
|
}
|
||||||
return OsmObject.isPolygon(this.tags)
|
return OsmObject.isPolygon(this.tags)
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default class FeaturePipelineState extends MapState {
|
||||||
constructor(layoutToUse: LayoutConfig) {
|
constructor(layoutToUse: LayoutConfig) {
|
||||||
super(layoutToUse);
|
super(layoutToUse);
|
||||||
|
|
||||||
const clustering = layoutToUse.clustering
|
const clustering = layoutToUse?.clustering
|
||||||
this.featureAggregator = TileHierarchyAggregator.createHierarchy(this);
|
this.featureAggregator = TileHierarchyAggregator.createHierarchy(this);
|
||||||
const clusterCounter = this.featureAggregator
|
const clusterCounter = this.featureAggregator
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
|
@ -117,10 +117,12 @@ export default class MapState extends UserRelatedState {
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
this.overlayToggles = this.layoutToUse.tileLayerSources.filter(c => c.name !== undefined).map(c => ({
|
this.overlayToggles = this.layoutToUse?.tileLayerSources
|
||||||
|
?.filter(c => c.name !== undefined)
|
||||||
|
?.map(c => ({
|
||||||
config: c,
|
config: c,
|
||||||
isDisplayed: QueryParameters.GetBooleanQueryParameter("overlay-" + c.id, c.defaultState, "Wether or not the overlay " + c.id + " is shown")
|
isDisplayed: QueryParameters.GetBooleanQueryParameter("overlay-" + c.id, c.defaultState, "Wether or not the overlay " + c.id + " is shown")
|
||||||
}))
|
})) ?? []
|
||||||
this.filteredLayers = this.InitializeFilteredLayers()
|
this.filteredLayers = this.InitializeFilteredLayers()
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,7 +144,7 @@ export default class MapState extends UserRelatedState {
|
||||||
initialized.add(overlayToggle.config)
|
initialized.add(overlayToggle.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tileLayerSource of this.layoutToUse.tileLayerSources) {
|
for (const tileLayerSource of this.layoutToUse?.tileLayerSources ?? []) {
|
||||||
if (initialized.has(tileLayerSource)) {
|
if (initialized.has(tileLayerSource)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -153,28 +155,14 @@ export default class MapState extends UserRelatedState {
|
||||||
|
|
||||||
private lockBounds() {
|
private lockBounds() {
|
||||||
const layout = this.layoutToUse;
|
const layout = this.layoutToUse;
|
||||||
if (layout.lockLocation) {
|
if (!layout?.lockLocation) {
|
||||||
if (layout.lockLocation === true) {
|
return;
|
||||||
const tile = Tiles.embedded_tile(
|
|
||||||
layout.startLat,
|
|
||||||
layout.startLon,
|
|
||||||
layout.startZoom - 1
|
|
||||||
);
|
|
||||||
const bounds = Tiles.tile_bounds(tile.z, tile.x, tile.y);
|
|
||||||
// We use the bounds to get a sense of distance for this zoom level
|
|
||||||
const latDiff = bounds[0][0] - bounds[1][0];
|
|
||||||
const lonDiff = bounds[0][1] - bounds[1][1];
|
|
||||||
layout.lockLocation = [
|
|
||||||
[layout.startLat - latDiff, layout.startLon - lonDiff],
|
|
||||||
[layout.startLat + latDiff, layout.startLon + lonDiff],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
console.warn("Locking the bounds to ", layout.lockLocation);
|
|
||||||
this.mainMapObject.installBounds(
|
|
||||||
new BBox(layout.lockLocation),
|
|
||||||
this.featureSwitchIsTesting.data
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
console.warn("Locking the bounds to ", layout.lockLocation);
|
||||||
|
this.mainMapObject.installBounds(
|
||||||
|
new BBox(layout.lockLocation),
|
||||||
|
this.featureSwitchIsTesting.data
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private initCurrentView() {
|
private initCurrentView() {
|
||||||
|
@ -364,8 +352,10 @@ export default class MapState extends UserRelatedState {
|
||||||
}
|
}
|
||||||
|
|
||||||
private InitializeFilteredLayers() {
|
private InitializeFilteredLayers() {
|
||||||
|
|
||||||
const layoutToUse = this.layoutToUse;
|
const layoutToUse = this.layoutToUse;
|
||||||
|
if(layoutToUse === undefined){
|
||||||
|
return new UIEventSource<FilteredLayer[]>([])
|
||||||
|
}
|
||||||
const flayers: FilteredLayer[] = [];
|
const flayers: FilteredLayer[] = [];
|
||||||
for (const layer of layoutToUse.layers) {
|
for (const layer of layoutToUse.layers) {
|
||||||
let isDisplayed: UIEventSource<boolean>
|
let isDisplayed: UIEventSource<boolean>
|
||||||
|
|
|
@ -127,7 +127,7 @@ export default class PointRenderingConfig extends WithContextLoader {
|
||||||
public GetBaseIcon(tags?: any): BaseUIElement {
|
public GetBaseIcon(tags?: any): BaseUIElement {
|
||||||
tags = tags ?? {id: "node/-1"}
|
tags = tags ?? {id: "node/-1"}
|
||||||
const rotation = Utils.SubstituteKeys(this.rotation?.GetRenderValue(tags)?.txt ?? "0deg", tags)
|
const rotation = Utils.SubstituteKeys(this.rotation?.GetRenderValue(tags)?.txt ?? "0deg", tags)
|
||||||
const htmlDefs = Utils.SubstituteKeys(this.icon.GetRenderValue(tags)?.txt, tags)
|
const htmlDefs = Utils.SubstituteKeys(this.icon?.GetRenderValue(tags)?.txt, tags)
|
||||||
let defaultPin: BaseUIElement = undefined
|
let defaultPin: BaseUIElement = undefined
|
||||||
if (this.label === undefined) {
|
if (this.label === undefined) {
|
||||||
defaultPin = Svg.teardrop_with_hole_green_svg()
|
defaultPin = Svg.teardrop_with_hole_green_svg()
|
||||||
|
|
|
@ -338,7 +338,8 @@ export default class TagRenderingConfig {
|
||||||
|
|
||||||
const free = this.freeform?.key
|
const free = this.freeform?.key
|
||||||
if (free !== undefined) {
|
if (free !== undefined) {
|
||||||
return tags[free] !== undefined
|
const value = tags[free]
|
||||||
|
return value !== undefined && value !== ""
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,7 @@ export default class Histogram<T> extends VariableUiElement {
|
||||||
keys.sort()
|
keys.sort()
|
||||||
break;
|
break;
|
||||||
case "name-rev":
|
case "name-rev":
|
||||||
keys.sort().reverse()
|
keys.sort().reverse(/*Copy of array, inplace reverse if fine*/)
|
||||||
break;
|
break;
|
||||||
case "count":
|
case "count":
|
||||||
keys.sort((k0, k1) => counts.get(k0) - counts.get(k1))
|
keys.sort((k0, k1) => counts.get(k0) - counts.get(k1))
|
||||||
|
|
|
@ -543,9 +543,9 @@ class LengthTextField extends TextFieldDef {
|
||||||
// Bit of a hack: we project the centerpoint to the closes point on the road - if available
|
// Bit of a hack: we project the centerpoint to the closes point on the road - if available
|
||||||
if (options?.feature !== undefined && options.feature.geometry.type !== "Point") {
|
if (options?.feature !== undefined && options.feature.geometry.type !== "Point") {
|
||||||
const lonlat = <[number, number]>[...options.location]
|
const lonlat = <[number, number]>[...options.location]
|
||||||
lonlat.reverse()
|
lonlat.reverse(/*Changes a clone, this is safe */)
|
||||||
options.location = <[number, number]>GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates
|
options.location = <[number, number]>GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates
|
||||||
options.location.reverse()
|
options.location.reverse(/*Changes a clone, this is safe */)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -373,7 +373,7 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
||||||
{
|
{
|
||||||
name: "max_snap_distance",
|
name: "max_snap_distance",
|
||||||
doc: "If the imported object is a LineString or (Multi)Polygon, already existing OSM-points will be reused to construct the geometry of the newly imported way",
|
doc: "If the imported object is a LineString or (Multi)Polygon, already existing OSM-points will be reused to construct the geometry of the newly imported way",
|
||||||
defaultValue: "5"
|
defaultValue: "0.05"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "move_osm_point_if",
|
name: "move_osm_point_if",
|
||||||
|
@ -381,7 +381,7 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
||||||
}, {
|
}, {
|
||||||
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: "0.05"
|
||||||
}, {
|
}, {
|
||||||
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",
|
||||||
|
@ -406,24 +406,12 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
||||||
AbstractImportButton.importedIds.add(originalFeatureTags.data.id)
|
AbstractImportButton.importedIds.add(originalFeatureTags.data.id)
|
||||||
const args = this.parseArgs(argument, originalFeatureTags)
|
const args = this.parseArgs(argument, originalFeatureTags)
|
||||||
const feature = state.allElements.ContainingFeatures.get(id)
|
const feature = state.allElements.ContainingFeatures.get(id)
|
||||||
console.log("Geometry to auto-import is:", feature)
|
|
||||||
const geom = feature.geometry
|
|
||||||
let coordinates: [number, number][]
|
|
||||||
if (geom.type === "LineString") {
|
|
||||||
coordinates = geom.coordinates
|
|
||||||
} else if (geom.type === "Polygon") {
|
|
||||||
coordinates = geom.coordinates[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const mergeConfigs = this.GetMergeConfig(args);
|
const mergeConfigs = this.GetMergeConfig(args);
|
||||||
|
const action = ImportWayButton.CreateAction(
|
||||||
const action = this.CreateAction(
|
|
||||||
feature,
|
feature,
|
||||||
args,
|
args,
|
||||||
<FeaturePipelineState>state,
|
<FeaturePipelineState>state,
|
||||||
mergeConfigs,
|
mergeConfigs
|
||||||
coordinates
|
|
||||||
)
|
)
|
||||||
await state.changes.applyAction(action)
|
await state.changes.applyAction(action)
|
||||||
}
|
}
|
||||||
|
@ -455,18 +443,8 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
||||||
|
|
||||||
|
|
||||||
// Upload the way to OSM
|
// Upload the way to OSM
|
||||||
const geom = feature.geometry
|
|
||||||
let coordinates: [number, number][]
|
|
||||||
if (geom.type === "LineString") {
|
|
||||||
coordinates = geom.coordinates
|
|
||||||
} else if (geom.type === "Polygon") {
|
|
||||||
coordinates = geom.coordinates[0]
|
|
||||||
}
|
|
||||||
const mergeConfigs = this.GetMergeConfig(args);
|
const mergeConfigs = this.GetMergeConfig(args);
|
||||||
|
let action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs);
|
||||||
|
|
||||||
let action = this.CreateAction(feature, args, state, mergeConfigs, coordinates);
|
|
||||||
|
|
||||||
return this.createConfirmPanelForWay(
|
return this.createConfirmPanelForWay(
|
||||||
state,
|
state,
|
||||||
args,
|
args,
|
||||||
|
@ -508,14 +486,12 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
||||||
return mergeConfigs;
|
return mergeConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CreateAction(feature,
|
private static CreateAction(feature,
|
||||||
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<any>; targetLayer: string },
|
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<any>; targetLayer: string },
|
||||||
state: FeaturePipelineState,
|
state: FeaturePipelineState,
|
||||||
mergeConfigs: any[],
|
mergeConfigs: any[]) {
|
||||||
coordinates: [number, number][]) {
|
|
||||||
|
|
||||||
const coors = feature.geometry.coordinates
|
const coors = feature.geometry.coordinates
|
||||||
if (feature.geometry.type === "Polygon" && coors.length > 1) {
|
if ((feature.geometry.type === "Polygon" ) && coors.length > 1) {
|
||||||
const outer = coors[0]
|
const outer = coors[0]
|
||||||
const inner = [...coors]
|
const inner = [...coors]
|
||||||
inner.splice(0, 1)
|
inner.splice(0, 1)
|
||||||
|
@ -531,7 +507,7 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
||||||
|
|
||||||
return new CreateWayWithPointReuseAction(
|
return new CreateWayWithPointReuseAction(
|
||||||
args.newTags.data,
|
args.newTags.data,
|
||||||
coordinates,
|
coors,
|
||||||
state,
|
state,
|
||||||
mergeConfigs
|
mergeConfigs
|
||||||
)
|
)
|
||||||
|
|
|
@ -28,7 +28,6 @@ export class LoginToggle extends VariableUiElement {
|
||||||
const login = new LoginButton(text, state)
|
const login = new LoginButton(text, state)
|
||||||
super(
|
super(
|
||||||
state.osmConnection.loadingStatus.map(osmConnectionState => {
|
state.osmConnection.loadingStatus.map(osmConnectionState => {
|
||||||
console.trace("Current osm state is ", osmConnectionState)
|
|
||||||
if(osmConnectionState === "loading"){
|
if(osmConnectionState === "loading"){
|
||||||
return loading
|
return loading
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ export default class SplitRoadWizard extends Toggle {
|
||||||
.filter(p => GeoOperations.distanceBetween(p[0].geometry.coordinates, coordinates) < 5)
|
.filter(p => GeoOperations.distanceBetween(p[0].geometry.coordinates, coordinates) < 5)
|
||||||
.map(p => p[1])
|
.map(p => p[1])
|
||||||
.sort((a, b) => a - b)
|
.sort((a, b) => a - b)
|
||||||
.reverse()
|
.reverse(/*Copy/derived list, inplace reverse is fine*/)
|
||||||
if (points.length > 0) {
|
if (points.length > 0) {
|
||||||
for (const point of points) {
|
for (const point of points) {
|
||||||
splitPoints.data.splice(point, 1)
|
splitPoints.data.splice(point, 1)
|
||||||
|
|
1
Utils.ts
1
Utils.ts
|
@ -777,5 +777,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
b: parseInt(hex.substr(5, 2), 16),
|
b: parseInt(hex.substr(5, 2), 16),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,34 +2,48 @@ import T from "./TestHelper";
|
||||||
import {exec} from "child_process";
|
import {exec} from "child_process";
|
||||||
|
|
||||||
export default class CodeQualitySpec extends T {
|
export default class CodeQualitySpec extends T {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super([
|
super([
|
||||||
[
|
[
|
||||||
"no constructor.name in compiled code", () => {
|
"no constructor.name in compiled code", () => {
|
||||||
|
CodeQualitySpec.detectInCode("constructor\\.name", "This is not allowed, as minification does erase names.")
|
||||||
const excludedDirs = [".git", "node_modules", "dist", ".cache", ".parcel-cache", "assets"]
|
}],
|
||||||
|
[
|
||||||
exec("grep \"constructor.name\" -r . " + excludedDirs.map(d => "--exclude-dir=" + d).join(" "), ((error, stdout, stderr) => {
|
"no reverse in compiled code", () => {
|
||||||
if (error?.message?.startsWith("Command failed: grep")) {
|
CodeQualitySpec.detectInCode("reverse()", "Reverse is stateful and changes the source list. This often causes subtle bugs")
|
||||||
return;
|
}]
|
||||||
}
|
|
||||||
if (error !== null) {
|
|
||||||
throw error
|
|
||||||
|
|
||||||
}
|
|
||||||
if (stderr !== "") {
|
|
||||||
throw stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
const found = stdout.split("\n").filter(s => s !== "").filter(s => s.startsWith("test/"));
|
|
||||||
if (found.length > 0) {
|
|
||||||
throw "Found a 'constructor.name' at " + found.join(", ") + ". This is not allowed, as minification does erase names."
|
|
||||||
}
|
|
||||||
|
|
||||||
}))
|
|
||||||
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param forbidden: a GREP-regex. This means that '.' is a wildcard and should be escaped to match a literal dot
|
||||||
|
* @param reason
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private static detectInCode(forbidden: string, reason: string) {
|
||||||
|
|
||||||
|
const excludedDirs = [".git", "node_modules", "dist", ".cache", ".parcel-cache", "assets"]
|
||||||
|
|
||||||
|
exec("grep -n \"" + forbidden + "\" -r . " + excludedDirs.map(d => "--exclude-dir=" + d).join(" "), ((error, stdout, stderr) => {
|
||||||
|
if (error?.message?.startsWith("Command failed: grep")) {
|
||||||
|
console.warn("Command failed!")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (error !== null) {
|
||||||
|
throw error
|
||||||
|
|
||||||
|
}
|
||||||
|
if (stderr !== "") {
|
||||||
|
throw stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
const found = stdout.split("\n").filter(s => s !== "").filter(s => !s.startsWith("./test/"));
|
||||||
|
if (found.length > 0) {
|
||||||
|
throw `Found a '${forbidden}' at \n ${found.join("\n ")}.\n ${reason}`
|
||||||
|
}
|
||||||
|
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
219
test/ImportMultiPolygon.spec.ts
Normal file
219
test/ImportMultiPolygon.spec.ts
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
import T from "./TestHelper";
|
||||||
|
import CreateMultiPolygonWithPointReuseAction from "../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction";
|
||||||
|
import { Tag } from "../Logic/Tags/Tag";
|
||||||
|
import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
|
||||||
|
import { Changes } from "../Logic/Osm/Changes";
|
||||||
|
import {ChangesetHandler} from "../Logic/Osm/ChangesetHandler";
|
||||||
|
import * as Assert from "assert";
|
||||||
|
|
||||||
|
export default class ImportMultiPolygonSpec extends T {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super([
|
||||||
|
["Correct changeset",
|
||||||
|
async () => {
|
||||||
|
|
||||||
|
const feature = {
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"osm_id": "41097039",
|
||||||
|
"size_grb_building": "1374.89",
|
||||||
|
"addr:housenumber": "53",
|
||||||
|
"addr:street": "Startelstraat",
|
||||||
|
"building": "house",
|
||||||
|
"source:geometry:entity": "Gbg",
|
||||||
|
"source:geometry:date": "2014-04-28",
|
||||||
|
"source:geometry:oidn": "150044",
|
||||||
|
"source:geometry:uidn": "5403181",
|
||||||
|
"H_DTM_MIN": "50.35",
|
||||||
|
"H_DTM_GEM": "50.97",
|
||||||
|
"H_DSM_MAX": "59.40",
|
||||||
|
"H_DSM_P99": "59.09",
|
||||||
|
"HN_MAX": "8.43",
|
||||||
|
"HN_P99": "8.12",
|
||||||
|
"detection_method": "derived from OSM landuse: farmyard",
|
||||||
|
"auto_target_landuse": "farmyard",
|
||||||
|
"size_source_landuse": "8246.28",
|
||||||
|
"auto_building": "farm",
|
||||||
|
"id": "41097039",
|
||||||
|
"_lat": "50.84633355000016",
|
||||||
|
"_lon": "5.262964150000011",
|
||||||
|
"_layer": "grb",
|
||||||
|
"_length": "185.06002152312757",
|
||||||
|
"_length:km": "0.2",
|
||||||
|
"_now:date": "2022-02-22",
|
||||||
|
"_now:datetime": "2022-02-22 10:15:51",
|
||||||
|
"_loaded:date": "2022-02-22",
|
||||||
|
"_loaded:datetime": "2022-02-22 10:15:51",
|
||||||
|
"_geometry:type": "Polygon",
|
||||||
|
"_intersects_with_other_features": "",
|
||||||
|
"_country": "be",
|
||||||
|
"_overlaps_with_buildings": "[]",
|
||||||
|
"_overlap_percentage": "null",
|
||||||
|
"_grb_date": "2014-04-28",
|
||||||
|
"_grb_ref": "Gbg/150044",
|
||||||
|
"_building:min_level": "",
|
||||||
|
"_surface": "548.1242491529038",
|
||||||
|
"_surface:ha": "0",
|
||||||
|
"_reverse_overlap_percentage": "null",
|
||||||
|
"_imported_osm_object_found": "false",
|
||||||
|
"_imported_osm_still_fresh": "false",
|
||||||
|
"_target_building_type": "house"
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": <[number, number][][]>[
|
||||||
|
[
|
||||||
|
[
|
||||||
|
5.262684300000043,
|
||||||
|
50.84624409999995
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.262777500000024,
|
||||||
|
50.84620759999988
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.262798899999998,
|
||||||
|
50.84621390000019
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.262999799999994,
|
||||||
|
50.84619519999999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.263107500000007,
|
||||||
|
50.84618920000014
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.263115,
|
||||||
|
50.84620990000026
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.26310279999998,
|
||||||
|
50.84623050000014
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.263117999999977,
|
||||||
|
50.846247400000166
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.263174599999989,
|
||||||
|
50.84631019999971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.263166999999989,
|
||||||
|
50.84631459999995
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.263243999999979,
|
||||||
|
50.84640239999989
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.2631607000000065,
|
||||||
|
50.84643459999996
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.26313309999997,
|
||||||
|
50.84640089999985
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.262907499999996,
|
||||||
|
50.84647790000018
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.2628939999999576,
|
||||||
|
50.846463699999774
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.262872100000033,
|
||||||
|
50.846440700000294
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.262784699999991,
|
||||||
|
50.846348899999924
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.262684300000043,
|
||||||
|
50.84624409999995
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
5.262801899999976,
|
||||||
|
50.84623269999982
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.2629535000000285,
|
||||||
|
50.84638830000012
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.263070700000018,
|
||||||
|
50.84634720000008
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.262998000000025,
|
||||||
|
50.84626279999982
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.263066799999966,
|
||||||
|
50.84623959999975
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.263064000000004,
|
||||||
|
50.84623330000007
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.263009599999997,
|
||||||
|
50.84623730000026
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.263010199999956,
|
||||||
|
50.84621629999986
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.262801899999976,
|
||||||
|
50.84623269999982
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerRings = [...feature.geometry.coordinates]
|
||||||
|
innerRings.splice(0, 1)
|
||||||
|
|
||||||
|
const action = new CreateMultiPolygonWithPointReuseAction(
|
||||||
|
[new Tag("building", "yes")],
|
||||||
|
feature.geometry.coordinates[0],
|
||||||
|
innerRings,
|
||||||
|
undefined,
|
||||||
|
[],
|
||||||
|
"import"
|
||||||
|
)
|
||||||
|
const descriptions = await action.Perform(new Changes())
|
||||||
|
|
||||||
|
function getCoor(id: number): {lat: number, lon:number} {
|
||||||
|
return <any> descriptions.find(d => d.type === "node" && d.id === id).changes
|
||||||
|
}
|
||||||
|
|
||||||
|
const ways= descriptions.filter(d => d.type === "way")
|
||||||
|
T.isTrue(ways[0].id == -18, "unexpected id")
|
||||||
|
T.isTrue(ways[1].id == -27, "unexpected id")
|
||||||
|
const outer = ways[0].changes["coordinates"]
|
||||||
|
const outerExpected = [[5.262684300000043,50.84624409999995],[5.262777500000024,50.84620759999988],[5.262798899999998,50.84621390000019],[5.262999799999994,50.84619519999999],[5.263107500000007,50.84618920000014],[5.263115,50.84620990000026],[5.26310279999998,50.84623050000014],[5.263117999999977,50.846247400000166],[5.263174599999989,50.84631019999971],[5.263166999999989,50.84631459999995],[5.263243999999979,50.84640239999989],[5.2631607000000065,50.84643459999996],[5.26313309999997,50.84640089999985],[5.262907499999996,50.84647790000018],[5.2628939999999576,50.846463699999774],[5.262872100000033,50.846440700000294],[5.262784699999991,50.846348899999924],[5.262684300000043,50.84624409999995]]
|
||||||
|
T.listIdentical(feature.geometry.coordinates[0], outer)
|
||||||
|
const inner = ways[1].changes["coordinates"]
|
||||||
|
T.listIdentical(feature.geometry.coordinates[1], inner)
|
||||||
|
const members = <{type: string, role: string, ref: number}[]> descriptions.find(d => d.type === "relation").changes["members"]
|
||||||
|
T.isTrue(members[0].role == "outer", "incorrect role")
|
||||||
|
T.isTrue(members[1].role == "inner", "incorrect role")
|
||||||
|
T.isTrue(members[0].type == "way", "incorrect type")
|
||||||
|
T.isTrue(members[1].type == "way", "incorrect type")
|
||||||
|
T.isTrue(members[0].ref == -18, "incorrect id")
|
||||||
|
T.isTrue(members[1].ref == -27, "incorrect id")
|
||||||
|
}]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import CreateNoteImportLayerSpec from "./CreateNoteImportLayer.spec";
|
||||||
import ValidatedTextFieldTranslationsSpec from "./ValidatedTextFieldTranslations.spec";
|
import ValidatedTextFieldTranslationsSpec from "./ValidatedTextFieldTranslations.spec";
|
||||||
import CreateCacheSpec from "./CreateCache.spec";
|
import CreateCacheSpec from "./CreateCache.spec";
|
||||||
import CodeQualitySpec from "./CodeQuality.spec";
|
import CodeQualitySpec from "./CodeQuality.spec";
|
||||||
|
import ImportMultiPolygonSpec from "./ImportMultiPolygon.spec";
|
||||||
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
@ -43,7 +44,8 @@ async function main() {
|
||||||
new CreateNoteImportLayerSpec(),
|
new CreateNoteImportLayerSpec(),
|
||||||
new ValidatedTextFieldTranslationsSpec(),
|
new ValidatedTextFieldTranslationsSpec(),
|
||||||
new CreateCacheSpec(),
|
new CreateCacheSpec(),
|
||||||
new CodeQualitySpec()
|
new CodeQualitySpec(),
|
||||||
|
new ImportMultiPolygonSpec()
|
||||||
]
|
]
|
||||||
ScriptUtils.fixUtils();
|
ScriptUtils.fixUtils();
|
||||||
const realDownloadFunc = Utils.externalDownloadFunction;
|
const realDownloadFunc = Utils.externalDownloadFunction;
|
||||||
|
|
Loading…
Reference in a new issue