feat: 矢量图层支持 geojson-vt (#1302)

* feat: 矢量图层支持 geojson-vt

* feat: 优化 parser 类型获取、内置 sourceLayer

* style: lint style

Co-authored-by: shihui <yiqianyao.yqy@alibaba-inc.com>
This commit is contained in:
YiQianYao 2022-08-23 20:02:17 +08:00 committed by GitHub
parent ae1aa59879
commit 1e9dfaa310
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 385 additions and 76 deletions

View File

@ -0,0 +1,2 @@
### geojson - vt
<code src="./geojson-vt.tsx"></code>

View File

@ -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 (
<div
id="map"
style={{
height: '500px',
position: 'relative',
}}
/>
);
};

View File

@ -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);

View File

@ -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(

View File

@ -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<ILineLayerStyleOptions> {
public type: string = 'LineLayer';
@ -49,9 +50,11 @@ export default class LineLayer extends BaseLayer<ILineLayerStyleOptions> {
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',
);

View File

@ -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<IPointLayerStyleOptions> {
public type: string = 'PointLayer';
@ -92,7 +93,8 @@ export default class PointLayer extends BaseLayer<IPointLayerStyleOptions> {
'earthFill',
'earthExtrude',
];
if (this.layerSource.parser.type === 'mvt') {
const parserType = this.layerSource.getParserType();
if (isVectorTile(parserType)) {
return 'vectorpoint';
}

View File

@ -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<IPolygonLayerStyleOptions> {
public type: string = 'PolygonLayer';
@ -29,9 +30,11 @@ export default class PolygonLayer extends BaseLayer<IPolygonLayerStyleOptions> {
}
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',
);

View File

@ -37,7 +37,8 @@ export default class RaterLayer extends BaseLayer<IRasterLayerStyleOptions> {
protected getModelType(): RasterModelType {
// 根据 source 的类型判断 model type
switch (this.layerSource.parser.type) {
const parserType = this.layerSource.getParserType();
switch (parserType) {
case 'raster':
return 'raster';
case 'rasterTile':

View File

@ -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,

View File

@ -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(

View File

@ -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",

View File

@ -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);

View File

@ -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;
}

View File

@ -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<TilesetManagerOptions> = {
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<MapboxVectorTile> => {
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<Geometries, Properties>,
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,
};
}

View File

@ -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<TilesetManagerOptions> = {
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) =>

View File

@ -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<TilesetManagerOptions> = {
@ -15,7 +11,7 @@ const DEFAULT_CONFIG: Partial<TilesetManagerOptions> = {
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) => {

View File

@ -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(