forked from MapComplete/MapComplete
Download button: take advantage of MVT server, download button will now attempt to download everything
This commit is contained in:
parent
bccda67e1c
commit
e4eb8d6b52
21 changed files with 453 additions and 353 deletions
|
@ -1,11 +1,11 @@
|
|||
import { ImmutableStore, Store } from "../../UIEventSource"
|
||||
import DynamicTileSource from "./DynamicTileSource"
|
||||
import { UpdatableDynamicTileSource } from "./DynamicTileSource"
|
||||
import { Utils } from "../../../Utils"
|
||||
import GeoJsonSource from "../Sources/GeoJsonSource"
|
||||
import { BBox } from "../../BBox"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
|
||||
export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
||||
export default class DynamicGeoJsonTileSource extends UpdatableDynamicTileSource {
|
||||
private static whitelistCache = new Map<string, any>()
|
||||
|
||||
constructor(
|
||||
|
@ -65,7 +65,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
|||
|
||||
const blackList = new Set<string>()
|
||||
super(
|
||||
new ImmutableStore(source.geojsonZoomLevel),
|
||||
new ImmutableStore(source.geojsonZoomLevel),
|
||||
layer.minzoom,
|
||||
(zxy) => {
|
||||
if (whitelist !== undefined) {
|
||||
|
|
|
@ -1,78 +1,16 @@
|
|||
import { Store } from "../../UIEventSource"
|
||||
import DynamicTileSource from "./DynamicTileSource"
|
||||
import { UpdatableDynamicTileSource } from "./DynamicTileSource"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { BBox } from "../../BBox"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import MvtSource from "../Sources/MvtSource"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import Constants from "../../../Models/Constants"
|
||||
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
||||
import { UpdatableFeatureSourceMerger } from "../Sources/FeatureSourceMerger"
|
||||
import { LineSourceMerger } from "./LineSourceMerger"
|
||||
import { PolygonSourceMerger } from "./PolygonSourceMerger"
|
||||
|
||||
|
||||
class PolygonMvtSource extends PolygonSourceMerger{
|
||||
constructor( layer: LayerConfig,
|
||||
mapProperties: {
|
||||
zoom: Store<number>
|
||||
bounds: Store<BBox>
|
||||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
}) {
|
||||
const roundedZoom = mapProperties.zoom.mapD(z => Math.min(Math.floor(z/2)*2, 14))
|
||||
super(
|
||||
roundedZoom,
|
||||
layer.minzoom,
|
||||
(zxy) => {
|
||||
const [z, x, y] = Tiles.tile_from_index(zxy)
|
||||
const url = Utils.SubstituteKeys(Constants.VectorTileServer,
|
||||
{
|
||||
z, x, y, layer: layer.id,
|
||||
type: "polygons",
|
||||
})
|
||||
return new MvtSource(url, x, y, z)
|
||||
},
|
||||
mapProperties,
|
||||
{
|
||||
isActive: options?.isActive,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LineMvtSource extends LineSourceMerger{
|
||||
constructor( layer: LayerConfig,
|
||||
mapProperties: {
|
||||
zoom: Store<number>
|
||||
bounds: Store<BBox>
|
||||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
}) {
|
||||
const roundedZoom = mapProperties.zoom.mapD(z => Math.min(Math.floor(z/2)*2, 14))
|
||||
super(
|
||||
roundedZoom,
|
||||
layer.minzoom,
|
||||
(zxy) => {
|
||||
const [z, x, y] = Tiles.tile_from_index(zxy)
|
||||
const url = Utils.SubstituteKeys(Constants.VectorTileServer,
|
||||
{
|
||||
z, x, y, layer: layer.id,
|
||||
type: "lines",
|
||||
})
|
||||
return new MvtSource(url, x, y, z)
|
||||
},
|
||||
mapProperties,
|
||||
{
|
||||
isActive: options?.isActive,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PointMvtSource extends DynamicTileSource {
|
||||
|
||||
class PolygonMvtSource extends PolygonSourceMerger {
|
||||
constructor(
|
||||
layer: LayerConfig,
|
||||
mapProperties: {
|
||||
|
@ -81,31 +19,32 @@ class PointMvtSource extends DynamicTileSource {
|
|||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
},
|
||||
}
|
||||
) {
|
||||
const roundedZoom = mapProperties.zoom.mapD(z => Math.min(Math.floor(z/2)*2, 14))
|
||||
const roundedZoom = mapProperties.zoom.mapD((z) => Math.min(Math.floor(z / 2) * 2, 14))
|
||||
super(
|
||||
roundedZoom,
|
||||
layer.minzoom,
|
||||
(zxy) => {
|
||||
const [z, x, y] = Tiles.tile_from_index(zxy)
|
||||
const url = Utils.SubstituteKeys(Constants.VectorTileServer,
|
||||
{
|
||||
z, x, y, layer: layer.id,
|
||||
type: "pois",
|
||||
})
|
||||
const url = Utils.SubstituteKeys(Constants.VectorTileServer, {
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
layer: layer.id,
|
||||
type: "polygons",
|
||||
})
|
||||
return new MvtSource(url, x, y, z)
|
||||
},
|
||||
mapProperties,
|
||||
{
|
||||
isActive: options?.isActive,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default class DynamicMvtileSource extends FeatureSourceMerger {
|
||||
|
||||
class LineMvtSource extends LineSourceMerger {
|
||||
constructor(
|
||||
layer: LayerConfig,
|
||||
mapProperties: {
|
||||
|
@ -114,13 +53,80 @@ export default class DynamicMvtileSource extends FeatureSourceMerger {
|
|||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
}
|
||||
) {
|
||||
const roundedZoom = mapProperties.zoom.mapD((z) => Math.min(Math.floor(z / 2) * 2, 14))
|
||||
super(
|
||||
roundedZoom,
|
||||
layer.minzoom,
|
||||
(zxy) => {
|
||||
const [z, x, y] = Tiles.tile_from_index(zxy)
|
||||
const url = Utils.SubstituteKeys(Constants.VectorTileServer, {
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
layer: layer.id,
|
||||
type: "lines",
|
||||
})
|
||||
return new MvtSource(url, x, y, z)
|
||||
},
|
||||
mapProperties,
|
||||
{
|
||||
isActive: options?.isActive,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class PointMvtSource extends UpdatableDynamicTileSource {
|
||||
constructor(
|
||||
layer: LayerConfig,
|
||||
mapProperties: {
|
||||
zoom: Store<number>
|
||||
bounds: Store<BBox>
|
||||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
}
|
||||
) {
|
||||
const roundedZoom = mapProperties.zoom.mapD((z) => Math.min(Math.floor(z / 2) * 2, 14))
|
||||
super(
|
||||
roundedZoom,
|
||||
layer.minzoom,
|
||||
(zxy) => {
|
||||
const [z, x, y] = Tiles.tile_from_index(zxy)
|
||||
const url = Utils.SubstituteKeys(Constants.VectorTileServer, {
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
layer: layer.id,
|
||||
type: "pois",
|
||||
})
|
||||
return new MvtSource(url, x, y, z)
|
||||
},
|
||||
mapProperties,
|
||||
{
|
||||
isActive: options?.isActive,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default class DynamicMvtileSource extends UpdatableFeatureSourceMerger {
|
||||
constructor(
|
||||
layer: LayerConfig,
|
||||
mapProperties: {
|
||||
zoom: Store<number>
|
||||
bounds: Store<BBox>
|
||||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
}
|
||||
) {
|
||||
super(
|
||||
new PointMvtSource(layer, mapProperties, options),
|
||||
new LineMvtSource(layer, mapProperties, options),
|
||||
new PolygonMvtSource(layer, mapProperties, options)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Store, Stores } from "../../UIEventSource"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
import { FeatureSource } from "../FeatureSource"
|
||||
import { FeatureSource, UpdatableFeatureSource } from "../FeatureSource"
|
||||
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
||||
|
||||
/***
|
||||
|
@ -11,6 +11,12 @@ import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
|||
export default class DynamicTileSource<
|
||||
Src extends FeatureSource = FeatureSource
|
||||
> extends FeatureSourceMerger<Src> {
|
||||
private readonly loadedTiles = new Set<number>()
|
||||
private readonly zDiff: number
|
||||
private readonly zoomlevel: Store<number>
|
||||
private readonly constructSource: (tileIndex: number) => Src
|
||||
private readonly bounds: Store<BBox>
|
||||
|
||||
/**
|
||||
*
|
||||
* @param zoomlevel If {z} is specified in the source, the 'zoomlevel' will be used as zoomlevel to download from
|
||||
|
@ -33,52 +39,86 @@ export default class DynamicTileSource<
|
|||
}
|
||||
) {
|
||||
super()
|
||||
const loadedTiles = new Set<number>()
|
||||
const zDiff = options?.zDiff ?? 0
|
||||
this.constructSource = constructSource
|
||||
this.zoomlevel = zoomlevel
|
||||
this.zDiff = options?.zDiff ?? 0
|
||||
this.bounds = mapProperties.bounds
|
||||
|
||||
const neededTiles: Store<number[]> = Stores.ListStabilized(
|
||||
mapProperties.bounds
|
||||
.mapD(
|
||||
(bounds) => {
|
||||
if (options?.isActive && !options?.isActive.data) {
|
||||
return undefined
|
||||
}
|
||||
.mapD(() => {
|
||||
if (options?.isActive && !options?.isActive.data) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (mapProperties.zoom.data < minzoom) {
|
||||
return undefined
|
||||
}
|
||||
const z = Math.floor(zoomlevel.data) + zDiff
|
||||
const tileRange = Tiles.TileRangeBetween(
|
||||
z,
|
||||
bounds.getNorth(),
|
||||
bounds.getEast(),
|
||||
bounds.getSouth(),
|
||||
bounds.getWest()
|
||||
)
|
||||
if (tileRange.total > 500) {
|
||||
console.warn(
|
||||
"Got a really big tilerange, bounds and location might be out of sync"
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const needed = Tiles.MapRange(tileRange, (x, y) =>
|
||||
Tiles.tile_index(z, x, y)
|
||||
).filter((i) => !loadedTiles.has(i))
|
||||
if (needed.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return needed
|
||||
},
|
||||
[options?.isActive, mapProperties.zoom]
|
||||
)
|
||||
if (mapProperties.zoom.data < minzoom) {
|
||||
return undefined
|
||||
}
|
||||
return this.getNeededTileIndices()
|
||||
}, [options?.isActive, mapProperties.zoom])
|
||||
.stabilized(250)
|
||||
)
|
||||
|
||||
neededTiles.addCallbackAndRunD((neededIndexes) => {
|
||||
for (const neededIndex of neededIndexes) {
|
||||
loadedTiles.add(neededIndex)
|
||||
super.addSource(constructSource(neededIndex))
|
||||
}
|
||||
})
|
||||
neededTiles.addCallbackAndRunD((neededIndexes) => this.downloadTiles(neededIndexes))
|
||||
}
|
||||
|
||||
protected downloadTiles(neededIndexes: number[]): Src[] {
|
||||
const sources: Src[] = []
|
||||
for (const neededIndex of neededIndexes) {
|
||||
this.loadedTiles.add(neededIndex)
|
||||
const src = this.constructSource(neededIndex)
|
||||
super.addSource(src)
|
||||
sources.push(src)
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
protected getNeededTileIndices() {
|
||||
const bounds = this.bounds.data
|
||||
const z = Math.floor(this.zoomlevel.data) + this.zDiff
|
||||
const tileRange = Tiles.TileRangeBetween(
|
||||
z,
|
||||
bounds.getNorth(),
|
||||
bounds.getEast(),
|
||||
bounds.getSouth(),
|
||||
bounds.getWest()
|
||||
)
|
||||
if (tileRange.total > 500) {
|
||||
console.warn("Got a really big tilerange, bounds and location might be out of sync")
|
||||
return []
|
||||
}
|
||||
const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(z, x, y)).filter(
|
||||
(i) => !this.loadedTiles.has(i)
|
||||
)
|
||||
if (needed.length === 0) {
|
||||
return []
|
||||
}
|
||||
return needed
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdatableDynamicTileSource<Src extends UpdatableFeatureSource = UpdatableFeatureSource>
|
||||
extends DynamicTileSource<Src>
|
||||
implements UpdatableFeatureSource
|
||||
{
|
||||
constructor(
|
||||
zoomlevel: Store<number>,
|
||||
minzoom: number,
|
||||
constructSource: (tileIndex: number) => Src,
|
||||
mapProperties: {
|
||||
bounds: Store<BBox>
|
||||
zoom: Store<number>
|
||||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
zDiff?: number
|
||||
}
|
||||
) {
|
||||
super(zoomlevel, minzoom, constructSource, mapProperties, options)
|
||||
}
|
||||
|
||||
async updateAsync() {
|
||||
const sources = super.downloadTiles(super.getNeededTileIndices())
|
||||
await Promise.all(sources.map((src) => src.updateAsync()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,31 @@
|
|||
import { FeatureSourceForTile } from "../FeatureSource"
|
||||
import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource"
|
||||
import { Store } from "../../UIEventSource"
|
||||
import { BBox } from "../../BBox"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { Feature, LineString, MultiLineString, Position } from "geojson"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { Feature, MultiLineString, Position } from "geojson"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import DynamicTileSource from "./DynamicTileSource"
|
||||
import { UpdatableDynamicTileSource } from "./DynamicTileSource"
|
||||
|
||||
/**
|
||||
* The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together.
|
||||
* This is used to reconstruct polygons of vector tiles
|
||||
*/
|
||||
export class LineSourceMerger extends DynamicTileSource<FeatureSourceForTile> {
|
||||
export class LineSourceMerger extends UpdatableDynamicTileSource<
|
||||
FeatureSourceForTile & UpdatableFeatureSource
|
||||
> {
|
||||
private readonly _zoomlevel: Store<number>
|
||||
|
||||
constructor(
|
||||
zoomlevel: Store<number>,
|
||||
minzoom: number,
|
||||
constructSource: (tileIndex: number) => FeatureSourceForTile,
|
||||
constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource,
|
||||
mapProperties: {
|
||||
bounds: Store<BBox>
|
||||
zoom: Store<number>
|
||||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
},
|
||||
}
|
||||
) {
|
||||
super(zoomlevel, minzoom, constructSource, mapProperties, options)
|
||||
this._zoomlevel = zoomlevel
|
||||
|
@ -35,33 +36,30 @@ export class LineSourceMerger extends DynamicTileSource<FeatureSourceForTile> {
|
|||
const all: Map<string, Feature<MultiLineString>> = new Map()
|
||||
const currentZoom = this._zoomlevel?.data ?? 0
|
||||
for (const source of sources) {
|
||||
if(source.z != currentZoom){
|
||||
if (source.z != currentZoom) {
|
||||
continue
|
||||
}
|
||||
const bboxCoors = Tiles.tile_bounds_lon_lat(source.z, source.x, source.y)
|
||||
const bboxGeo = new BBox(bboxCoors).asGeoJson({})
|
||||
for (const f of source.features.data) {
|
||||
const id = f.properties.id
|
||||
const coordinates : Position[][] = []
|
||||
if(f.geometry.type === "LineString"){
|
||||
const coordinates: Position[][] = []
|
||||
if (f.geometry.type === "LineString") {
|
||||
coordinates.push(f.geometry.coordinates)
|
||||
}else if(f.geometry.type === "MultiLineString"){
|
||||
} else if (f.geometry.type === "MultiLineString") {
|
||||
coordinates.push(...f.geometry.coordinates)
|
||||
}else {
|
||||
} else {
|
||||
console.error("Invalid geometry type:", f.geometry.type)
|
||||
continue
|
||||
}
|
||||
const oldV = all.get(id)
|
||||
if(!oldV){
|
||||
|
||||
all.set(id, {
|
||||
type: "Feature",
|
||||
properties: f.properties,
|
||||
geometry:{
|
||||
type:"MultiLineString",
|
||||
coordinates
|
||||
}
|
||||
})
|
||||
if (!oldV) {
|
||||
all.set(id, {
|
||||
type: "Feature",
|
||||
properties: f.properties,
|
||||
geometry: {
|
||||
type: "MultiLineString",
|
||||
coordinates,
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
oldV.geometry.coordinates.push(...coordinates)
|
||||
|
@ -70,11 +68,13 @@ export class LineSourceMerger extends DynamicTileSource<FeatureSourceForTile> {
|
|||
|
||||
const keys = Array.from(all.keys())
|
||||
for (const key of keys) {
|
||||
all.set(key, <any> GeoOperations.attemptLinearize(<Feature<MultiLineString>>all.get(key)))
|
||||
all.set(
|
||||
key,
|
||||
<any>GeoOperations.attemptLinearize(<Feature<MultiLineString>>all.get(key))
|
||||
)
|
||||
}
|
||||
const newList = Array.from(all.values())
|
||||
this.features.setData(newList)
|
||||
this._featuresById.setData(all)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
import { FeatureSourceForTile } from "../FeatureSource"
|
||||
import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource"
|
||||
import { Store } from "../../UIEventSource"
|
||||
import { BBox } from "../../BBox"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { Feature } from "geojson"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import DynamicTileSource from "./DynamicTileSource"
|
||||
import DynamicTileSource, { UpdatableDynamicTileSource } from "./DynamicTileSource"
|
||||
|
||||
/**
|
||||
* The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together.
|
||||
* This is used to reconstruct polygons of vector tiles
|
||||
*/
|
||||
export class PolygonSourceMerger extends DynamicTileSource<FeatureSourceForTile> {
|
||||
export class PolygonSourceMerger extends UpdatableDynamicTileSource<
|
||||
FeatureSourceForTile & UpdatableFeatureSource
|
||||
> {
|
||||
constructor(
|
||||
zoomlevel: Store<number>,
|
||||
minzoom: number,
|
||||
constructSource: (tileIndex: number) => FeatureSourceForTile,
|
||||
constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource,
|
||||
mapProperties: {
|
||||
bounds: Store<BBox>
|
||||
zoom: Store<number>
|
||||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
},
|
||||
}
|
||||
) {
|
||||
super(zoomlevel, minzoom, constructSource, mapProperties, options)
|
||||
}
|
||||
|
@ -69,5 +71,4 @@ export class PolygonSourceMerger extends DynamicTileSource<FeatureSourceForTile>
|
|||
this.features.setData(newList)
|
||||
this._featuresById.setData(all)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@ export class SummaryTileSourceRewriter implements FeatureSource {
|
|||
private filteredLayers: FilteredLayer[]
|
||||
public readonly features: Store<Feature[]> = this._features
|
||||
private readonly _summarySource: SummaryTileSource
|
||||
private readonly _totalNumberOfFeatures: UIEventSource<number> = new UIEventSource<number>(
|
||||
undefined
|
||||
)
|
||||
public readonly totalNumberOfFeatures: Store<number> = this._totalNumberOfFeatures
|
||||
constructor(
|
||||
summarySource: SummaryTileSource,
|
||||
filteredLayers: ReadonlyMap<string, FilteredLayer>
|
||||
|
@ -31,6 +35,7 @@ export class SummaryTileSourceRewriter implements FeatureSource {
|
|||
}
|
||||
|
||||
private update() {
|
||||
let fullTotal = 0
|
||||
const newFeatures: Feature[] = []
|
||||
const layersToCount = this.filteredLayers.filter((fl) => fl.isDisplayed.data)
|
||||
const bitmap = layersToCount.map((l) => (l.isDisplayed.data ? "1" : "0")).join("")
|
||||
|
@ -42,10 +47,17 @@ export class SummaryTileSourceRewriter implements FeatureSource {
|
|||
}
|
||||
newFeatures.push({
|
||||
...f,
|
||||
properties: { ...f.properties, id: f.properties.id + bitmap, total: newTotal },
|
||||
properties: {
|
||||
...f.properties,
|
||||
id: f.properties.id + bitmap,
|
||||
total: newTotal,
|
||||
total_metric: Utils.numberWithMetrixPrefix(newTotal),
|
||||
},
|
||||
})
|
||||
fullTotal += newTotal
|
||||
}
|
||||
this._features.setData(newFeatures)
|
||||
this._totalNumberOfFeatures.setData(fullTotal)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,7 +106,7 @@ export class SummaryTileSource extends DynamicTileSource {
|
|||
}
|
||||
const lat = counts["lat"]
|
||||
const lon = counts["lon"]
|
||||
const total = Utils.numberWithMetrixPrefix(Number(counts["total"]))
|
||||
const total = Number(counts["total"])
|
||||
const tileBbox = new BBox(Tiles.tile_bounds_lon_lat(z, x, y))
|
||||
if (!tileBbox.contains([lon, lat])) {
|
||||
console.error(
|
||||
|
@ -116,6 +128,7 @@ export class SummaryTileSource extends DynamicTileSource {
|
|||
summary: "yes",
|
||||
...counts,
|
||||
total,
|
||||
total_metric: Utils.numberWithMetrixPrefix(total),
|
||||
layers: layersSummed,
|
||||
},
|
||||
geometry: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue