@mesh3d/cesium-vectortile-gl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ import { VectorTileFeature } from "@mapbox/vector-tile"
2
+ import { ILayerVisualizer } from "./ILayerVisualizer"
3
+ import { SymbolRenderLayer } from "../SymbolRenderLayer"
4
+ import { warnOnce } from "maplibre-gl/src/util/util"
5
+
6
+ export class SymbolFeature {
7
+ constructor() {
8
+ this.featureId = 0
9
+ this.textColor = Cesium.Color.BLACK.clone()
10
+ this.textSize = 12
11
+ this.coordinates = []
12
+ }
13
+ }
14
+
15
+ /**@type {Cesium.Cartesian3} */
16
+ let scratchDirectionToEye = null
17
+ /**@type {Cesium.Cartesian3} */
18
+ let scratchSurfaceNormal = null
19
+
20
+ export class SymbolLayerVisualizer extends ILayerVisualizer {
21
+ constructor(layers, tile) {
22
+ if (scratchDirectionToEye === null) {
23
+ scratchDirectionToEye = new Cesium.Cartesian3()
24
+ scratchSurfaceNormal = new Cesium.Cartesian3()
25
+ }
26
+
27
+ super(layers, tile)
28
+
29
+ /**@type {Cesium.Label[]} */
30
+ this.labels = []
31
+ this.primitive = null
32
+ this.dotCutOff = 0.0035
33
+ }
34
+
35
+ /**
36
+ * 对符号进行地平线剔除
37
+ * @param {Cesium.Cartesian3} positionWC
38
+ * @param {Cesium.Cartesian3} cameraPositionWC
39
+ */
40
+ isOccluded(cameraPositionWC, positionWC) {
41
+ /*
42
+ 如下图,o为符号锚点,up为椭球面过锚点o的法线,tangent为过锚点o的切线,eye为相机位置。
43
+ 可见,当锚点在地平线以下时,eye方向与up方向夹角小于90°。
44
+ up
45
+ ^
46
+ |
47
+ o —— —— > tangent
48
+ \
49
+ \
50
+ eye
51
+ */
52
+ const eyeDir = Cesium.Cartesian3.subtract(cameraPositionWC, positionWC, scratchDirectionToEye)
53
+ Cesium.Cartesian3.normalize(eyeDir, eyeDir)
54
+ const up = Cesium.Cartesian3.normalize(positionWC, scratchSurfaceNormal)
55
+ return Cesium.Cartesian3.dot(eyeDir, up) < this.dotCutOff
56
+ }
57
+
58
+ /**
59
+ * @param {VectorTileFeature[]} features
60
+ * @param {SymbolRenderLayer} layer
61
+ * @param {Cesium.frameState} frameState
62
+ * @param {VectorTileset} tileset
63
+ */
64
+ addLayer(features, layer, frameState, tileset) {
65
+ const style = layer.style
66
+ const { tile, labels } = this
67
+ const rectangle = tile.rectangle
68
+
69
+ function addText(coord, text, font, textSize, textColor, outlineWidth, outlineColor) {
70
+ if (!Cesium.Rectangle.contains(rectangle, Cesium.Cartographic.fromDegrees(coord[0], coord[1]))) {
71
+ return
72
+ }
73
+ const label = new Cesium.Label({
74
+ position: Cesium.Cartesian3.fromDegrees(coord[0], coord[1]),
75
+ text,
76
+ font: textSize + 'px ' + font,
77
+ fillColor: textColor,
78
+ style: outlineWidth && Cesium.LabelStyle.FILL_AND_OUTLINE,
79
+ outlineWidth: outlineWidth * textSize,
80
+ outlineColor,
81
+ //禁用深度测试
82
+ disableDepthTestDistance: Infinity
83
+ })
84
+ labels.push(label)
85
+ layer.labels.push(label)
86
+ }
87
+
88
+ for (const sourceFeature of features) {
89
+ const feature = sourceFeature.toGeoJSON(tile.x, tile.y, tile.z)
90
+ if (!feature.geometry) continue
91
+ const properties = sourceFeature.properties
92
+
93
+ //读取图层样式属性
94
+ const iconImage = style.layout.getDataValue('icon-image', tile.z, sourceFeature)
95
+ const textField = style.layout.getDataValue('text-field', tile.z, sourceFeature)
96
+ let text = textField && style.layout.resolveTokens(properties, textField)
97
+ if (iconImage) {
98
+ warnOnce('symbol图层:不支持图标')
99
+ continue
100
+ }
101
+ if (!text) {
102
+ continue
103
+ }
104
+ const maxWidth = style.layout.getDataValue('text-max-width', tile.z, sourceFeature) * 3
105
+ const textRotationAlignment = style.layout.getDataValue('text-rotation-alignment', tile.z, sourceFeature)
106
+ const textPitchAlignment = style.layout.getDataValue('text-pitch-alignment', tile.z, sourceFeature)
107
+ if (text.length > maxWidth) {
108
+ warnOnce('symbol图层: 不支持 text-max-width,无自动换行效果')
109
+ }
110
+ if (textRotationAlignment === 'map') {
111
+ warnOnce('symbol图层:text-rotation-alignment 仅支持 viewport')
112
+ }
113
+ if (textPitchAlignment === 'map') {
114
+ warnOnce('symbol图层:text-pitch-alignment 仅支持 viewport')
115
+ }
116
+
117
+ const font = style.layout.getDataValue('text-font', tile.z, sourceFeature)
118
+ const textSize = style.layout.getDataValue('text-size', tile.z, sourceFeature)
119
+ const textColor = style.convertColor(style.paint.getDataValue('text-color', tile.z, sourceFeature))
120
+ const outlineColor = style.convertColor(style.paint.getDataValue('text-halo-color', tile.z, sourceFeature))
121
+ const outlineWidth = style.paint.getDataValue('text-halo-width', tile.z, sourceFeature)
122
+
123
+ const geometryType = feature.geometry.type
124
+ const coordinates = feature.geometry.coordinates
125
+ if (geometryType == 'Point') {
126
+ addText(coordinates, text, font, textSize, textColor, outlineWidth, outlineColor)
127
+ }
128
+ else if (geometryType == 'MultiPoint') {
129
+ coordinates.forEach(coord => {
130
+ addText(coord, text, font, textSize, textColor, outlineWidth, outlineColor)
131
+ })
132
+ }
133
+ else {
134
+ warnOnce('symbol图层:不支持符号沿线布局');
135
+ }
136
+ }
137
+
138
+ this.layers.push(layer)
139
+ }
140
+
141
+ createPrimitive() {
142
+ //所有图层的文字共用一个LabelCollection
143
+ //注意:这样文字就没有了“图层”的特征了,渲染顺序可能和样式配置的不一致
144
+ //优化:参考 maplibre-gl 的符号系统实现,但工作量巨大,如有需求建议使用商业版(Mesh-3D矢量地图引擎)
145
+
146
+ const primitive = new Cesium.LabelCollection()
147
+ for (let i = 0; i < this.labels.length; i++) {
148
+ this.labels[i] = primitive.add(this.labels[i])
149
+ }
150
+ this.primitive = primitive
151
+ }
152
+
153
+ update(frameState, tileset) {
154
+ if (!this.primitive && this.labels?.length) {
155
+ this.createPrimitive()
156
+ }
157
+
158
+ //我们禁用了 label 的深度测试,这里剔除中心位置在地球背面的符号
159
+ const cameraPositionWC = frameState.camera.positionWC
160
+ for (const label of this.labels) {
161
+ label.show = !this.isOccluded(cameraPositionWC, label.position)
162
+ }
163
+
164
+ //性能优化:这里应该进行自动避让处理
165
+
166
+ if (this.primitive) {
167
+ this.primitive.update(frameState)
168
+ }
169
+ }
170
+
171
+ destroy() {
172
+ this.primitive = this.primitive && this.primitive.destroy()
173
+ super.destroy()
174
+ }
175
+
176
+ isDestroyed() {
177
+ return false
178
+ }
179
+ }
@@ -0,0 +1,46 @@
1
+ import { ISource } from "./ISource";
2
+ import { registerSource } from "./registerSource";
3
+ import geojsonvt from "geojson-vt";
4
+ import { GeoJSONWrapper } from '@maplibre/vt-pbf'
5
+ import { EXTENT } from 'maplibre-gl/src/data/extent';
6
+
7
+ export class GeoJSONSource extends ISource {
8
+ constructor(styleSource, path) {
9
+ super(styleSource, path)
10
+ }
11
+
12
+ async init() {
13
+ /**@type {import("@maplibre/maplibre-gl-style-spec").GeoJSONSourceSpecification} */
14
+ const sourceParams = this.styleSource
15
+ let data = sourceParams.data
16
+ if (typeof data === 'string') {
17
+ const url = /^((http)|(https)|(data:)|\/)/.test(data) ? data : this.path + data
18
+ try {
19
+ data = await Cesium.Resource.fetchJson(url)
20
+ } catch (err) {
21
+ this.errorEvent.raiseEvent(err)
22
+ }
23
+ }
24
+ if (data && data.features?.length) {
25
+ this.tileIndex = new geojsonvt(data, {
26
+ extent: EXTENT,
27
+ buffer: sourceParams.buffer === undefined ? 128 : sourceParams.buffer,
28
+ tolerance: sourceParams.tolerance === sourceParams.tolerance ? 0.375 : sourceParams.tolerance
29
+ })
30
+ }
31
+ }
32
+
33
+ async requestTile(x, y, z) {
34
+ if (!this.tileIndex) return
35
+ try {
36
+ const geoJSONTile = this.tileIndex.getTile(z, x, y)
37
+ if (!geoJSONTile) return
38
+ const geojsonWrapper = new GeoJSONWrapper(geoJSONTile.features, { extent: EXTENT });
39
+ return geojsonWrapper
40
+ } catch (err) {
41
+ this.errorEvent.raiseEvent(err)
42
+ }
43
+ }
44
+ }
45
+
46
+ registerSource('geojson', GeoJSONSource)
@@ -0,0 +1,39 @@
1
+ import { VectorTile } from '@mapbox/vector-tile'
2
+
3
+ /**
4
+ * 数据源基类,定义通用属性和必须实现的方法(init、requestTile)
5
+ * @see VectorSource
6
+ * @see GeoJSONSource
7
+ */
8
+ export class ISource {
9
+ /**
10
+ * 构造数据源实例。注意:该构造函数由VectorTileset调用,请勿在其他模块直接调用
11
+ * @param {import('@maplibre/maplibre-gl-style-spec').SourceSpecification} styleSource
12
+ * @param {string} [path]
13
+ * @see VectorSource
14
+ * @see GeoJSONSource
15
+ */
16
+ constructor(styleSource, path = '') {
17
+ /**@type {"vector"|"geojson"} */
18
+ this.type = styleSource.type
19
+ /**@type {import('@maplibre/maplibre-gl-style-spec').SourceSpecification} */
20
+ this.styleSource = styleSource
21
+ this.path = path
22
+ this.errorEvent = new Cesium.Event()
23
+ }
24
+
25
+ async init() { }
26
+
27
+ /**
28
+ * @param {number} x
29
+ * @param {number} y
30
+ * @param {number} z
31
+ * @returns {Promise<VectorTile>}
32
+ */
33
+ requestTile(x, y, z) { }
34
+
35
+ destroy() {
36
+ this.styleSource = null
37
+ this.errorEvent = null
38
+ }
39
+ }
@@ -0,0 +1,45 @@
1
+ import { ISource } from "./ISource";
2
+ import { registerSource } from "./registerSource";
3
+ import { VectorTile } from '@mapbox/vector-tile'
4
+ import Pbf from 'pbf'
5
+
6
+ export class VectorSource extends ISource {
7
+ constructor(styleSource, path) {
8
+ super(styleSource, path)
9
+ }
10
+
11
+ async init() {
12
+ const sourceParams = this.styleSource
13
+ let url = sourceParams.url
14
+ if (url && !sourceParams.tiles) {
15
+ url = /^((http)|(https)|(data:)|\/)/.test(url) ? url : this.path + sourceParams.url
16
+ try {
17
+ const metadata = await Cesium.Resource.fetchJson(url)
18
+ for (const key in metadata) {
19
+ if (!sourceParams[key]) {
20
+ sourceParams[key] = metadata[key]
21
+ }
22
+ }
23
+ } catch (err) {
24
+ this.errorEvent.raiseEvent(err)
25
+ }
26
+ }
27
+ }
28
+
29
+ async requestTile(x, y, z) {
30
+ const sourceParams = this.styleSource
31
+ if (!sourceParams.tiles || !sourceParams.tiles.length) return
32
+ let tileUrl = sourceParams.tiles[0].replace('{x}', x).replace('{y}', y).replace('{z}', z)
33
+ tileUrl = /^((http)|(https)|(data:)|\/)/.test(tileUrl) ? tileUrl : this.path + tileUrl
34
+
35
+ try {
36
+ const tileBuf = await (fetch(tileUrl).then(res => res.arrayBuffer()));
37
+ const tileData = new VectorTile(new Pbf(tileBuf))
38
+ return tileData
39
+ } catch (err) {
40
+ this.errorEvent.raiseEvent(err)
41
+ }
42
+ }
43
+ }
44
+
45
+ registerSource('vector', VectorSource)
@@ -0,0 +1,20 @@
1
+ import { SubdivisionGranularitySetting, SubdivisionGranularityExpression } from "maplibre-gl/src/render/subdivision_granularity_settings"
2
+
3
+ const granularitySettings = {
4
+ globe: new SubdivisionGranularitySetting({
5
+ fill: new SubdivisionGranularityExpression(128, 2),
6
+ line: new SubdivisionGranularityExpression(512, 0),
7
+ // Always keep at least some subdivision on raster tiles, etc,
8
+ // otherwise they will be visibly warped at high zooms (before mercator transition).
9
+ // This si not needed on fill, because fill geometry tends to already be
10
+ // highly tessellated and granular at high zooms.
11
+ tile: new SubdivisionGranularityExpression(128, 32),
12
+ // Stencil granularity must never be higher than fill granularity,
13
+ // otherwise we would get seams in the oceans at zoom levels where
14
+ // stencil has higher granularity than fill.
15
+ stencil: new SubdivisionGranularityExpression(128, 1),
16
+ circle: 3
17
+ })
18
+ }
19
+
20
+ export { granularitySettings }
@@ -0,0 +1,11 @@
1
+
2
+ import { ISource } from './ISource'
3
+ import { registerSource, Sources } from './registerSource'
4
+ import { VectorSource } from './VectorSource'
5
+ import { GeoJSONSource } from './GeoJSONSource'
6
+
7
+ export {
8
+ ISource, registerSource, Sources,
9
+ VectorSource,
10
+ GeoJSONSource
11
+ }
@@ -0,0 +1,19 @@
1
+ import { ISource } from "./ISource"
2
+
3
+ /**
4
+ * @type {{[sourceType:string]:typeof ISource}}
5
+ */
6
+ const Sources = {}
7
+
8
+ /**
9
+ * 注册数据源类型,设置数据源类
10
+ * @param {*} type
11
+ * @param {*} sourceCls
12
+ */
13
+ function registerSource(type, sourceCls) {
14
+ Sources[type] = sourceCls
15
+ }
16
+
17
+ export {
18
+ Sources, registerSource
19
+ }
@@ -0,0 +1,43 @@
1
+ import { featureFilter, Color } from "@maplibre/maplibre-gl-style-spec";
2
+ import { StyleLayerProperties } from "./StyleLayerProperties";
3
+
4
+ export class StyleLayer {
5
+ /**
6
+ * @param {import('@maplibre/maplibre-gl-style-spec').LayerSpecification} layer
7
+ */
8
+ constructor(layer) {
9
+ this.data = layer
10
+ this.type = layer.type
11
+ this.id = layer.id
12
+ this.minzoom = layer.minzoom || 0
13
+ this.maxzoom = layer.maxzoom || 24
14
+ this.source = layer.source
15
+ /**@type {string} */
16
+ this.sourceLayer = layer['source-layer']
17
+ /**@type {import("@maplibre/maplibre-gl-style-spec").FeatureFilter|null} */
18
+ this.filter = null
19
+ this.paint = new StyleLayerProperties('paint_' + layer.type, layer.paint)
20
+ this.layout = new StyleLayerProperties('layout_' + layer.type, layer.layout)
21
+ if (layer.filter) {
22
+ this.filter = featureFilter(layer.filter)
23
+ }
24
+ }
25
+
26
+ /**
27
+ * 转换图层样式颜色,内部进行预乘Alpha的逆处理,@maplibre/maplibre-gl-style-spec内部会自动对颜色进行premultiplyAlpha操作,直接使用会出现明显的色差
28
+ * @param {Color} styleColor
29
+ * @param {Cesium.Color} [result]
30
+ * @returns
31
+ */
32
+ convertColor(styleColor, result) {
33
+ const alphaScalar = styleColor.a > 0 ? 1. / styleColor.a : 1
34
+ if (!result) {
35
+ result = new Cesium.Color()
36
+ }
37
+ result.red = styleColor.r * alphaScalar
38
+ result.green = styleColor.g * alphaScalar
39
+ result.blue = styleColor.b * alphaScalar
40
+ result.alpha = styleColor.a
41
+ return result
42
+ }
43
+ }
@@ -0,0 +1,43 @@
1
+ import { expression, latest } from '@maplibre/maplibre-gl-style-spec'
2
+
3
+ export class StyleLayerProperties {
4
+ constructor(groupName, styleProperties = {}) {
5
+ this.data = styleProperties
6
+ /**@type {Map<string,import('@maplibre/maplibre-gl-style-spec').StylePropertyExpression>} */
7
+ this.props = new Map()
8
+
9
+ const groupReference = latest[groupName]
10
+ for (const key in groupReference) {
11
+ if (Object.hasOwnProperty.call(groupReference, key)) {
12
+ const reference = groupReference[key];
13
+ const value = styleProperties[key];
14
+ const property = expression.normalizePropertyExpression(
15
+ value === undefined ? reference.default : value, reference,
16
+ )
17
+ this.props.set(key, property)
18
+ }
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Replace tokens in a string template with values in an object
24
+ *
25
+ * @param properties - a key/value relationship between tokens and replacements
26
+ * @param text - the template string
27
+ * @returns the template with tokens replaced
28
+ */
29
+ resolveTokens(properties, text) {
30
+ return text.replace(/{([^{}]+)}/g, (match, key) => {
31
+ return properties && key in properties ? String(properties[key]) : '';
32
+ });
33
+ }
34
+ getDataConstValue(name, zoom) {
35
+ const expr = this.props.get(name)
36
+ return expr && expr.evaluate({ zoom })
37
+ }
38
+
39
+ getDataValue(name, zoom, feature) {
40
+ const expr = this.props.get(name)
41
+ return expr && expr.evaluate({ zoom }, feature)
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ export { StyleLayerProperties } from "./StyleLayerProperties";
2
+ export { StyleLayer } from "./StyleLayer";