diff --git a/dev-demos/features/tile/geojson-vt.md b/dev-demos/features/tile/geojson-vt.md new file mode 100644 index 0000000000..fc6139abaa --- /dev/null +++ b/dev-demos/features/tile/geojson-vt.md @@ -0,0 +1,2 @@ +### geojson - vt + \ No newline at end of file diff --git a/dev-demos/features/tile/geojson-vt.tsx b/dev-demos/features/tile/geojson-vt.tsx new file mode 100644 index 0000000000..57d13f0936 --- /dev/null +++ b/dev-demos/features/tile/geojson-vt.tsx @@ -0,0 +1,72 @@ +// @ts-ignore +import { Scene, Source, PolygonLayer } from '@antv/l7'; +// @ts-ignore +import { Mapbox } from '@antv/l7-maps'; +import React, { useEffect } from 'react'; + +export default () => { + useEffect(() => { + const scene = new Scene({ + id: 'map', + stencil: true, + map: new Mapbox({ + center: [121.268, 30.3628], + pitch: 0, + style: 'blank', + zoom: 4, + }), + }); + + fetch( + 'https://gw.alipayobjects.com/os/bmw-prod/2b7aae6e-5f40-437f-8047-100e9a0d2808.json', + ) + .then((d) => d.json()) + .then((data) => { + const source = new Source(data, { + parser: { + type: 'geojsonvt', + tileSize: 256, + zoomOffset: 0, + maxZoom: 9, + extent: [-180, -85.051129, 179, 85.051129], + }, + }); + + // const line = new LineLayer({ + // featureId: 'COLOR', + // sourceLayer: 'testName', // woods hillshade contour ecoregions ecoregions2 city + // }).source(source) + // .color('COLOR') + // .size(2) + // scene.addLayer(line); + + const polygon = new PolygonLayer({ + featureId: 'COLOR', + // sourceLayer: 'testName', // woods hillshade contour ecoregions ecoregions2 city + }) + .source(source) + .color('COLOR') + .active(true); + scene.addLayer(polygon); + + // const point = new PointLayer({ + // featureId: 'COLOR', + // sourceLayer: 'testName', // woods hillshade contour ecoregions ecoregions2 city + // }) + // .source(source) + // // .color('COLOR') + // .color('#f00') + // .size(20) + // scene.addLayer(point); + }); + }, []); + return ( +
+ ); +}; diff --git a/dev-demos/features/tile/vectortile.tsx b/dev-demos/features/tile/vectortile.tsx index 20d2172e2d..44ee82e5da 100644 --- a/dev-demos/features/tile/vectortile.tsx +++ b/dev-demos/features/tile/vectortile.tsx @@ -4,57 +4,6 @@ import { Scene, LineLayer } from '@antv/l7'; import { Mapbox } from '@antv/l7-maps'; import React, { useEffect } from 'react'; -const list = [ - { - value: -28.0, - color1: 'orange', - province_adcode: '630000', - province_adName: '青海省', - province: '青海省', - nnh: 2, - }, - { - value: 29.0, - color1: 'orange', - province_adcode: '640000', - province_adName: '宁夏回族自治区', - province: '宁夏回族自治区', - nnh: 3, - }, - { - value: 60.0, - color1: 'orange', - province_adcode: '650000', - province_adName: '新疆维吾尔自治区', - province: '新疆维吾尔自治区', - nnh: 4, - }, - { - value: -31.0, - color1: 'orange', - province_adcode: '710000', - province_adName: '台湾省', - province: '台湾省', - nnh: 4, - }, - { - value: 80.0, - color1: 'orange', - province_adcode: '810000', - province_adName: '香港特别行政区', - province: '香港特别行政区', - nnh: 4, - }, - { - value: -33.0, - color1: 'orange', - province_adcode: '820000', - province_adName: '澳门特别行政区', - province: '澳门特别行政区', - nnh: 4, - }, -]; - export default () => { useEffect(() => { const scene = new Scene({ @@ -83,18 +32,10 @@ export default () => { maxZoom: 9, extent: [-180, -85.051129, 179, 85.051129], }, - transforms: [ - { - type: 'join', - sourceField: 'nnh', - targetField: 'NNH', // data 对应字段名 绑定到的地理数据 - data: list, - }, - ], }, ) .shape('simple') - // .shape('line') + .color('COLOR') .size(2) .select(true); diff --git a/packages/core/src/services/source/ISourceService.ts b/packages/core/src/services/source/ISourceService.ts index 3fa6894591..d27a23df60 100644 --- a/packages/core/src/services/source/ISourceService.ts +++ b/packages/core/src/services/source/ISourceService.ts @@ -73,6 +73,7 @@ export interface ISource { updateClusterData(zoom: number): void; getFeatureById(id: number): unknown; getFeatureId(field: string, value: any): number | undefined; + getParserType(): string; getClusters(zoom: number): any; getClustersLeaves(id: number): any; updateFeaturePropertiesById( diff --git a/packages/layers/src/line/index.ts b/packages/layers/src/line/index.ts index 0f9eb7994b..ec2098619f 100644 --- a/packages/layers/src/line/index.ts +++ b/packages/layers/src/line/index.ts @@ -1,6 +1,7 @@ import BaseLayer from '../core/BaseLayer'; import { ILineLayerStyleOptions } from '../core/interface'; import LineModels, { LineModelType } from './models'; +import { isVectorTile } from '../tile/utils'; export default class LineLayer extends BaseLayer { public type: string = 'LineLayer'; @@ -49,9 +50,11 @@ export default class LineLayer extends BaseLayer { if (this.layerType) { return this.layerType as LineModelType; } - if (this.layerSource.parser.type === 'mvt') { + const parserType = this.layerSource.getParserType(); + if (isVectorTile(parserType)) { return 'vectorline'; } + const shapeAttribute = this.styleAttributeService.getLayerStyleAttribute( 'shape', ); diff --git a/packages/layers/src/point/index.ts b/packages/layers/src/point/index.ts index 7492b53e69..4e33ea0548 100644 --- a/packages/layers/src/point/index.ts +++ b/packages/layers/src/point/index.ts @@ -2,6 +2,7 @@ import { IEncodeFeature } from '@antv/l7-core'; import BaseLayer from '../core/BaseLayer'; import { IPointLayerStyleOptions } from '../core/interface'; import PointModels, { PointType } from './models/index'; +import { isVectorTile } from '../tile/utils'; export default class PointLayer extends BaseLayer { public type: string = 'PointLayer'; @@ -92,7 +93,8 @@ export default class PointLayer extends BaseLayer { 'earthFill', 'earthExtrude', ]; - if (this.layerSource.parser.type === 'mvt') { + const parserType = this.layerSource.getParserType(); + if (isVectorTile(parserType)) { return 'vectorpoint'; } diff --git a/packages/layers/src/polygon/index.ts b/packages/layers/src/polygon/index.ts index 089d0f5509..3afdb4eed0 100644 --- a/packages/layers/src/polygon/index.ts +++ b/packages/layers/src/polygon/index.ts @@ -2,6 +2,7 @@ import { IEncodeFeature } from '@antv/l7-core'; import BaseLayer from '../core/BaseLayer'; import { IPolygonLayerStyleOptions } from '../core/interface'; import PolygonModels, { PolygonModelType } from './models/'; +import { isVectorTile } from '../tile/utils'; export default class PolygonLayer extends BaseLayer { public type: string = 'PolygonLayer'; @@ -29,9 +30,11 @@ export default class PolygonLayer extends BaseLayer { } protected getModelType(): PolygonModelType { - if (this.layerSource.parser.type === 'mvt') { + const parserType = this.layerSource.getParserType(); + if (isVectorTile(parserType)) { return 'vectorpolygon'; } + const shapeAttribute = this.styleAttributeService.getLayerStyleAttribute( 'shape', ); diff --git a/packages/layers/src/raster/index.ts b/packages/layers/src/raster/index.ts index 754eec0c51..95bfa02ede 100644 --- a/packages/layers/src/raster/index.ts +++ b/packages/layers/src/raster/index.ts @@ -37,7 +37,8 @@ export default class RaterLayer extends BaseLayer { protected getModelType(): RasterModelType { // 根据 source 的类型判断 model type - switch (this.layerSource.parser.type) { + const parserType = this.layerSource.getParserType(); + switch (parserType) { case 'raster': return 'raster'; case 'rasterTile': diff --git a/packages/layers/src/tile/manager/tileLayerManager.ts b/packages/layers/src/tile/manager/tileLayerManager.ts index 6796ce9de4..89f5a68130 100644 --- a/packages/layers/src/tile/manager/tileLayerManager.ts +++ b/packages/layers/src/tile/manager/tileLayerManager.ts @@ -175,6 +175,7 @@ export class TileLayerManager implements ITileLayerManager { ); const source = this.parent.getSource(); const { coords } = source?.data?.tilesetOptions || {}; + const parentParserType = source.getParserType(); const layerShape = getLayerShape(this.parent.type, this.parent); @@ -189,7 +190,7 @@ export class TileLayerManager implements ITileLayerManager { shape: layerShape, zIndex, opacity, - sourceLayer, + sourceLayer: parentParserType === 'geojsonvt' ? 'geojsonvt' : sourceLayer, coords, featureId, color: colorValue, diff --git a/packages/layers/src/tile/utils.ts b/packages/layers/src/tile/utils.ts index b1b1754abc..c207dbf47f 100644 --- a/packages/layers/src/tile/utils.ts +++ b/packages/layers/src/tile/utils.ts @@ -6,6 +6,13 @@ import { } from '@antv/l7-core'; import { DOM, Tile } from '@antv/l7-utils'; import { Container } from 'inversify'; + +export const tileVectorParser = ['mvt', 'geojsonvt']; + +export function isVectorTile(parserType: string) { + return tileVectorParser.indexOf(parserType) > 0; +} + export function registerLayers(parentLayer: ILayer, layers: ILayer[]) { layers.map((layer) => { const container = createLayerContainer( diff --git a/packages/source/package.json b/packages/source/package.json index 591477b31b..0e2e6e10c4 100644 --- a/packages/source/package.json +++ b/packages/source/package.json @@ -30,6 +30,7 @@ "@babel/runtime": "^7.7.7", "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/vector-tile": "^1.3.1", + "geojson-vt": "^3.2.1", "@turf/helpers": "^6.1.4", "@turf/invariant": "^6.1.2", "@turf/meta": "^6.0.2", @@ -43,6 +44,7 @@ "supercluster": "^7.0.0" }, "devDependencies": { + "@types/geojson-vt": "3.2.0", "@types/d3-dsv": "^1.0.36", "@types/d3-hexbin": "^0.2.3", "@types/lodash": "^4.14.138", diff --git a/packages/source/src/index.ts b/packages/source/src/index.ts index 2d7063b771..2ddaf700a8 100644 --- a/packages/source/src/index.ts +++ b/packages/source/src/index.ts @@ -4,6 +4,7 @@ import geojson from './parser/geojson'; import image from './parser/image'; import json, { defaultData, defaultParser, defaultSource } from './parser/json'; import mapboxVectorTile from './parser/mvt'; +import geojsonVTTile from './parser/geojsonvt'; import raster from './parser/raster'; import rasterTile from './parser/raster-tile'; import Source from './source'; @@ -16,6 +17,7 @@ import { map } from './transform/map'; registerParser('rasterTile', rasterTile); registerParser('mvt', mapboxVectorTile); +registerParser('geojsonvt', geojsonVTTile); registerParser('geojson', geojson); registerParser('image', image); registerParser('csv', csv); diff --git a/packages/source/src/interface.ts b/packages/source/src/interface.ts index 426f9129b6..b339829779 100644 --- a/packages/source/src/interface.ts +++ b/packages/source/src/interface.ts @@ -27,7 +27,20 @@ export enum RasterTileType { IMAGE = 'image', ARRAYBUFFER = 'arraybuffer', } -export interface IRasterTileParserCFG { + +export interface IGeojsonvtOptions { + maxZoom: number; // max zoom to preserve detail on + indexMaxZoom: number; // max zoom in the tile index + indexMaxPoints: number; // max number of points per tile in the tile index + tolerance: number; // simplification tolerance (higher means simpler) + extent: number; // tile extent + buffer: number; // tile buffer on each side + lineMetrics: boolean; // whether to calculate line metrics + promoteId: null; // name of a feature property to be promoted to feature.id + generateId: boolean; // whether to generate feature ids. Cannot be used with promoteId + debug: number; // logging level (0, 1 or 2) +} +export interface ITileParserCFG { tileSize?: number; minZoom?: number; maxZoom?: number; @@ -42,6 +55,8 @@ export interface IRasterTileParserCFG { // 指定栅格瓦片的类型 dataType?: RasterTileType; + geojsonvtOptions?: IGeojsonvtOptions; + format?: any; } diff --git a/packages/source/src/parser/geojsonvt.ts b/packages/source/src/parser/geojsonvt.ts new file mode 100644 index 0000000000..f77eed68d8 --- /dev/null +++ b/packages/source/src/parser/geojsonvt.ts @@ -0,0 +1,259 @@ +import { Tile, TileLoadParams, TilesetManagerOptions } from '@antv/l7-utils'; +import { + Feature, + FeatureCollection, + Geometries, + Properties, +} from '@turf/helpers'; +import geojsonvt from 'geojson-vt'; +import { VectorTileLayer } from '@mapbox/vector-tile'; +import { IParserData, ITileParserCFG, IGeojsonvtOptions } from '../interface'; + +const DEFAULT_CONFIG: Partial = { + tileSize: 256, + minZoom: 0, + maxZoom: Infinity, + zoomOffset: 0, +}; + +function signedArea(ring: any[]) { + let sum = 0; + for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) { + p1 = ring[i]; + p2 = ring[j]; + sum += (p2.x - p1.x) * (p1.y + p2.y); + } + return sum; +} + +function classifyRings(rings: any[]) { + const len = rings.length; + + if (len <= 1) { + return [rings]; + } + + const polygons: any = []; + let polygon: any; + let ccw; + + for (let i = 0; i < len; i++) { + const area = signedArea(rings[i]); + if (area === 0) { + continue; + } + + if (ccw === undefined) { + ccw = area < 0; + } + + if (ccw === area < 0) { + if (polygon) { + polygons.push(polygon); + } + polygon = [rings[i]]; + } else { + polygon.push(rings[i]); + } + } + if (polygon) { + polygons.push(polygon); + } + + return polygons; +} + +const VectorTileFeatureTypes = ['Unknown', 'Point', 'LineString', 'Polygon']; + +function GetGeoJSON( + extent: number, + x: number, + y: number, + z: number, + vectorTileFeature: any, +) { + let coords = vectorTileFeature.geometry as any; + const currenType = vectorTileFeature.type; + + const currentProperties = vectorTileFeature.tags; + const currentId = vectorTileFeature.id; + + const size = extent * Math.pow(2, z); + const x0 = extent * x; + const y0 = extent * y; + + let type = VectorTileFeatureTypes[currenType]; + let i; + let j; + + function project(line: any[]) { + for (let j = 0; j < line.length; j++) { + const p = line[j]; + if (p[3]) { + // 避免重复计算 + break; + } + const y2 = 180 - ((p[1] + y0) * 360) / size; + const lng = ((p[0] + x0) * 360) / size - 180; + const lat = + (360 / Math.PI) * Math.atan(Math.exp((y2 * Math.PI) / 180)) - 90; + line[j] = [lng, lat, 0, 1]; + } + } + + switch (currenType) { + case 1: + const points = []; + for (i = 0; i < coords.length; i++) { + points[i] = coords[i][0]; + } + coords = points; + project(coords); + break; + + case 2: + for (i = 0; i < coords.length; i++) { + project(coords[i]); + } + break; + + case 3: + coords = classifyRings(coords); + for (i = 0; i < coords.length; i++) { + for (j = 0; j < coords[i].length; j++) { + project(coords[i][j]); + } + } + break; + } + + if (coords.length === 1) { + coords = coords[0]; + } else { + type = 'Multi' + type; + } + + const result = { + type: 'Feature', + geometry: { + type, + coordinates: coords, + }, + properties: currentProperties, + id: currentId, + tileOrigin: [0, 0], + coord: '', + }; + + return result; +} + +export type MapboxVectorTile = { + layers: { [_: string]: VectorTileLayer & { features: Feature[] } }; +}; + +const getVectorTile = async ( + tile: Tile, + tileIndex: any, + tileParams: TileLoadParams, + extent: number, +): Promise => { + return new Promise((resolve) => { + const tileData = tileIndex.getTile(tile.z, tile.x, tile.y); + // tileData + const features: any = []; + tileData.features.map((vectorTileFeature: any) => { + const feature = GetGeoJSON( + extent, + tileParams.x, + tileParams.y, + tileParams.z, + vectorTileFeature, + ); + features.push(feature); + }); + + const vectorTile = { + layers: { + // Tip: fixed SourceLayer Name + geojsonvt: { + features, + } as VectorTileLayer & { + features: Feature[]; + }, + }, + } as MapboxVectorTile; + + resolve(vectorTile); + }); +}; + +function getGeoJSONVTOptions(cfg?: ITileParserCFG) { + const defaultOptions = { + // geojson-vt default options + maxZoom: 14, // max zoom to preserve detail on + indexMaxZoom: 5, // max zoom in the tile index + indexMaxPoints: 100000, // max number of points per tile in the tile index + tolerance: 3, // simplification tolerance (higher means simpler) + extent: 4096, // tile extent + buffer: 64, // tile buffer on each side + lineMetrics: false, // whether to calculate line metrics + promoteId: null, // name of a feature property to be promoted to feature.id + generateId: true, // whether to generate feature ids. Cannot be used with promoteId + debug: 0, // logging level (0, 1 or 2) + }; + + if (cfg === undefined || typeof cfg.geojsonvtOptions === 'undefined') { + return defaultOptions; + } else { + cfg.geojsonvtOptions.maxZoom && + (defaultOptions.maxZoom = cfg.geojsonvtOptions.maxZoom); + cfg.geojsonvtOptions.indexMaxZoom && + (defaultOptions.indexMaxZoom = cfg.geojsonvtOptions.indexMaxZoom); + cfg.geojsonvtOptions.indexMaxPoints && + (defaultOptions.indexMaxPoints = cfg.geojsonvtOptions.indexMaxPoints); + cfg.geojsonvtOptions.tolerance && + (defaultOptions.tolerance = cfg.geojsonvtOptions.tolerance); + cfg.geojsonvtOptions.extent && + (defaultOptions.extent = cfg.geojsonvtOptions.extent); + cfg.geojsonvtOptions.buffer && + (defaultOptions.buffer = cfg.geojsonvtOptions.buffer); + cfg.geojsonvtOptions.lineMetrics && + (defaultOptions.lineMetrics = cfg.geojsonvtOptions.lineMetrics); + cfg.geojsonvtOptions.promoteId && + (defaultOptions.promoteId = cfg.geojsonvtOptions.promoteId); + cfg.geojsonvtOptions.generateId && + (defaultOptions.generateId = cfg.geojsonvtOptions.generateId); + cfg.geojsonvtOptions.debug && + (defaultOptions.debug = cfg.geojsonvtOptions.debug); + return defaultOptions; + } +} + +export default function geojsonVTTile( + data: FeatureCollection, + cfg: ITileParserCFG, +): IParserData { + const geojsonOptions = getGeoJSONVTOptions(cfg) as geojsonvt.Options & + IGeojsonvtOptions; + + const extent = geojsonOptions.extent || 4096; + const tileIndex = geojsonvt(data, geojsonOptions); + + const getTileData = (tileParams: TileLoadParams, tile: Tile) => { + return getVectorTile(tile, tileIndex, tileParams, extent); + }; + + const tilesetOptions = { + ...DEFAULT_CONFIG, + ...cfg, + getTileData, + }; + + return { + data, + dataArray: [], + tilesetOptions, + isTile: true, + }; +} diff --git a/packages/source/src/parser/mvt.ts b/packages/source/src/parser/mvt.ts index 6843abf528..33bbb936d1 100644 --- a/packages/source/src/parser/mvt.ts +++ b/packages/source/src/parser/mvt.ts @@ -12,7 +12,7 @@ import { } from '@mapbox/vector-tile'; import { Feature } from '@turf/helpers'; import Protobuf from 'pbf'; -import { IParserData, IRasterTileParserCFG } from '../interface'; +import { IParserData, ITileParserCFG } from '../interface'; const DEFAULT_CONFIG: Partial = { tileSize: 256, @@ -214,7 +214,7 @@ const getVectorTile = async ( export default function mapboxVectorTile( data: string | string[], - cfg?: IRasterTileParserCFG, + cfg?: ITileParserCFG, ): IParserData { const coord = cfg?.coord || 'lnglat'; // lnglat - offset const getTileData = (tileParams: TileLoadParams, tile: Tile) => diff --git a/packages/source/src/parser/raster-tile.ts b/packages/source/src/parser/raster-tile.ts index 18af7def24..13e9152850 100644 --- a/packages/source/src/parser/raster-tile.ts +++ b/packages/source/src/parser/raster-tile.ts @@ -1,9 +1,5 @@ import { Tile, TileLoadParams, TilesetManagerOptions } from '@antv/l7-utils'; -import { - IParserData, - IRasterTileParserCFG, - RasterTileType, -} from '../interface'; +import { IParserData, ITileParserCFG, RasterTileType } from '../interface'; import { defaultFormat, getTileBuffer, getTileImage } from '../utils/getTile'; const DEFAULT_CONFIG: Partial = { @@ -15,7 +11,7 @@ const DEFAULT_CONFIG: Partial = { export default function rasterTile( data: string | string[], - cfg?: IRasterTileParserCFG, + cfg?: ITileParserCFG, ): IParserData { const tileDataType: RasterTileType = cfg?.dataType || RasterTileType.IMAGE; const getTileData = (tileParams: TileLoadParams, tile: Tile) => { diff --git a/packages/source/src/source.ts b/packages/source/src/source.ts index 6635857839..d10e3958d2 100644 --- a/packages/source/src/source.ts +++ b/packages/source/src/source.ts @@ -2,7 +2,6 @@ import { SyncHook } from '@antv/async-hook'; import { IClusterOptions, - IMapService, IParseDataItem, IParserCfg, IParserData, @@ -57,7 +56,6 @@ export default class Source extends EventEmitter implements ISource { // 瓦片数据管理器 public tileset: TilesetManager | undefined; - private readonly mapService: IMapService; // 是否有效范围 private invalidExtent: boolean = false; @@ -87,6 +85,10 @@ export default class Source extends EventEmitter implements ISource { return this.clusterIndex.getLeaves(id, Infinity); } + public getParserType() { + return this.parser.type; + } + public updateClusterData(zoom: number): void { const { method = 'sum', field } = this.clusterOptions; let data = this.clusterIndex.getClusters(