@naivemap/mapbox-gl-image-layer 0.4.2 → 0.6.0-beta.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.
- package/README.md +25 -4
- package/dist/es/ImageLayer.js +232 -0
- package/dist/es/arrugator/Arrugator.js +270 -0
- package/dist/es/{utils/arrugator.js → arrugator/index.js} +43 -39
- package/dist/es/index.js +2 -132
- package/dist/es/shaders/image.fragment.glsl.js +1 -0
- package/dist/es/shaders/image.vertex.glsl.js +1 -0
- package/dist/es/shaders/mask.fragment.glsl.js +1 -0
- package/dist/es/shaders/mask.vertex.glsl.js +1 -0
- package/dist/js/ImageLayer.d.ts +62 -0
- package/dist/js/ImageLayer.js +270 -0
- package/dist/js/arrugator/Arrugator.d.ts +18 -0
- package/dist/js/arrugator/Arrugator.js +275 -0
- package/dist/js/arrugator/index.d.ts +7 -0
- package/dist/js/{utils/arrugator.js → arrugator/index.js} +49 -46
- package/dist/js/index.d.ts +4 -45
- package/dist/js/index.js +7 -134
- package/dist/js/shaders/image.fragment.glsl.d.ts +2 -0
- package/dist/js/shaders/image.fragment.glsl.js +3 -0
- package/dist/js/shaders/image.vertex.glsl.d.ts +2 -0
- package/dist/js/shaders/image.vertex.glsl.js +3 -0
- package/dist/js/shaders/mask.fragment.glsl.d.ts +2 -0
- package/dist/js/shaders/mask.fragment.glsl.js +3 -0
- package/dist/js/shaders/mask.vertex.glsl.d.ts +2 -0
- package/dist/js/shaders/mask.vertex.glsl.js +3 -0
- package/package.json +9 -6
- package/LICENSE +0 -21
- package/dist/es/utils/image.js +0 -19
- package/dist/es/utils/webgl.js +0 -64
- package/dist/js/utils/arrugator.d.ts +0 -7
- package/dist/js/utils/image.d.ts +0 -7
- package/dist/js/utils/image.js +0 -23
- package/dist/js/utils/webgl.d.ts +0 -8
- package/dist/js/utils/webgl.js +0 -68
package/README.md
CHANGED
|
@@ -31,14 +31,26 @@ export default class ImageLayer implements mapboxgl.CustomLayerInterface
|
|
|
31
31
|
| **option.coordinates** <br />(`Array<Array<number>>`) | Corners of image specified in longitude, latitude pairs: top left, top right, bottom right, bottom left. ref: [coordinates](https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#image-coordinates) |
|
|
32
32
|
| **option.resampling** <br />(Optional `enum`. One of `"linear"`, `"nearest"`. Defaults to `"linear"`) | The resampling/interpolation method to use for overscaling, also known as texture magnification filter. ref: [raster-resampling](https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#paint-raster-raster-resampling) |
|
|
33
33
|
| **option.opacity** <br />(Optional `number` between 0 and 1 inclusive. Defaults to 1. | The opacity at which the image will be drawn. |
|
|
34
|
-
| **
|
|
34
|
+
| **option.crossOrigin** <br />(`string`) | The crossOrigin attribute is a string which specifies the Cross-Origin Resource Sharing ([CORS](https://developer.mozilla.org/en-US/docs/Glossary/CORS)) setting to use when retrieving the image. |
|
|
35
|
+
| **option.mask** <br />(`MaskProperty`) | The polygonal mask or multipolygonal mask for the image. |
|
|
35
36
|
|
|
36
37
|
```ts
|
|
37
38
|
export type ImageOption = {
|
|
38
39
|
url: string
|
|
39
40
|
projection: string
|
|
40
41
|
coordinates: Coordinates
|
|
41
|
-
resampling
|
|
42
|
+
resampling?: 'linear' | 'nearest'
|
|
43
|
+
opacity?: number
|
|
44
|
+
crossOrigin?: string
|
|
45
|
+
mask?: MaskProperty
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// top left, top right, bottom right, bottom left.
|
|
49
|
+
export type Coordinates = [[number, number], [number, number], [number, number], [number, number]]
|
|
50
|
+
|
|
51
|
+
export type MaskProperty = {
|
|
52
|
+
type?: 'in' | 'out' // default: in
|
|
53
|
+
data: GeoJSON.Polygon | GeoJSON.MultiPolygon
|
|
42
54
|
}
|
|
43
55
|
```
|
|
44
56
|
|
|
@@ -46,17 +58,26 @@ export type ImageOption = {
|
|
|
46
58
|
|
|
47
59
|
#### updateImage
|
|
48
60
|
|
|
49
|
-
Updates the
|
|
61
|
+
Updates the URL, the projection, the coordinates, the opacity or the resampling of the image.
|
|
50
62
|
|
|
51
63
|
```ts
|
|
52
64
|
updateImage(option: {
|
|
53
|
-
url
|
|
65
|
+
url?: string
|
|
54
66
|
projection?: string
|
|
55
67
|
coordinates?: Coordinates
|
|
68
|
+
opacity?: number
|
|
56
69
|
resampling?: 'linear' | 'nearest'
|
|
57
70
|
}): this
|
|
58
71
|
```
|
|
59
72
|
|
|
73
|
+
#### updateMask
|
|
74
|
+
|
|
75
|
+
Updates the mask property.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
updateMask(mask: Partial<MaskProperty>): this
|
|
79
|
+
```
|
|
80
|
+
|
|
60
81
|
## Example
|
|
61
82
|
|
|
62
83
|
```ts
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { satisfies } from 'compare-versions';
|
|
2
|
+
import earcut, { flatten } from 'earcut';
|
|
3
|
+
import mapboxgl from 'mapbox-gl';
|
|
4
|
+
import * as twgl from 'twgl.js';
|
|
5
|
+
import { initArrugator } from './arrugator';
|
|
6
|
+
import fs from './shaders/image.fragment.glsl';
|
|
7
|
+
import vs from './shaders/image.vertex.glsl';
|
|
8
|
+
import maskfs from './shaders/mask.fragment.glsl';
|
|
9
|
+
import maskvs from './shaders/mask.vertex.glsl';
|
|
10
|
+
var ImageLayer = /** @class */ (function () {
|
|
11
|
+
function ImageLayer(id, option) {
|
|
12
|
+
this.type = 'custom';
|
|
13
|
+
this.renderingMode = '2d';
|
|
14
|
+
// mask
|
|
15
|
+
this.stencilChecked = true; // resetStencilClippingMasks 版本检查
|
|
16
|
+
this.id = id;
|
|
17
|
+
this.option = option;
|
|
18
|
+
this.loaded = false;
|
|
19
|
+
this.maskProperty = Object.assign({ type: 'in' }, option.mask);
|
|
20
|
+
this.metadata = option.metadata;
|
|
21
|
+
// 检查 stencil 是否可用
|
|
22
|
+
this.stencilChecked = satisfies(mapboxgl.version, '>=2.7.0');
|
|
23
|
+
// 如果传了 mask 边界数据,且版本不符
|
|
24
|
+
if (this.maskProperty.data && !this.stencilChecked) {
|
|
25
|
+
throw new Error("\u5982\u679C\u9700\u8981\u906E\u7F69\uFF08\u63A9\u819C\uFF09\uFF0Cmapbox-gl \u7248\u672C\u5FC5\u987B\uFF1A>=2.7.0");
|
|
26
|
+
}
|
|
27
|
+
// 初始化 Arrugator
|
|
28
|
+
var projection = option.projection, coordinates = option.coordinates;
|
|
29
|
+
this.arrugado = initArrugator(projection, coordinates, option.arrugatorStep);
|
|
30
|
+
}
|
|
31
|
+
ImageLayer.prototype.onAdd = function (map, gl) {
|
|
32
|
+
this.map = map;
|
|
33
|
+
this.gl = gl;
|
|
34
|
+
// 主程序
|
|
35
|
+
this.programInfo = twgl.createProgramInfo(gl, [vs, fs]);
|
|
36
|
+
this.loadTexture(map, gl);
|
|
37
|
+
this.bufferInfo = twgl.createBufferInfoFromArrays(gl, {
|
|
38
|
+
a_pos: { numComponents: 2, data: this.arrugado.pos },
|
|
39
|
+
a_uv: { numComponents: 2, data: this.arrugado.uv },
|
|
40
|
+
indices: this.arrugado.trigs,
|
|
41
|
+
});
|
|
42
|
+
// 掩膜程序
|
|
43
|
+
if (this.maskProperty.data) {
|
|
44
|
+
var data = this.maskProperty.data;
|
|
45
|
+
if (data) {
|
|
46
|
+
this.maskProgramInfo = twgl.createProgramInfo(gl, [maskvs, maskfs]);
|
|
47
|
+
this.maskBufferInfo = this.getMaskBufferInfo(gl, data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
ImageLayer.prototype.onRemove = function (_, gl) {
|
|
52
|
+
if (this.programInfo) {
|
|
53
|
+
gl.deleteProgram(this.programInfo.program);
|
|
54
|
+
}
|
|
55
|
+
if (this.maskProgramInfo) {
|
|
56
|
+
gl.deleteProgram(this.maskProgramInfo.program);
|
|
57
|
+
}
|
|
58
|
+
if (this.texture) {
|
|
59
|
+
gl.deleteTexture(this.texture);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
ImageLayer.prototype.render = function (gl, matrix) {
|
|
63
|
+
/**
|
|
64
|
+
* 线图层在启用 stencil 会消失,参考: https://github.com/mapbox/mapbox-gl-js/issues/12213
|
|
65
|
+
* 临时解决方案: map.painter.resetStencilClippingMasks()
|
|
66
|
+
* 该方法在 mapboxgl version >=2.7.0 才能用
|
|
67
|
+
*/
|
|
68
|
+
var _a, _b;
|
|
69
|
+
if (this.stencilChecked) {
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
this.map.painter.resetStencilClippingMasks();
|
|
72
|
+
}
|
|
73
|
+
if (this.loaded && this.programInfo && this.bufferInfo) {
|
|
74
|
+
// blend
|
|
75
|
+
gl.enable(gl.BLEND);
|
|
76
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
77
|
+
if (this.maskProgramInfo && this.maskBufferInfo) {
|
|
78
|
+
// mask program
|
|
79
|
+
gl.useProgram(this.maskProgramInfo.program);
|
|
80
|
+
// stencil test
|
|
81
|
+
gl.enable(gl.STENCIL_TEST);
|
|
82
|
+
gl.stencilFunc(gl.ALWAYS, 1, 0xff);
|
|
83
|
+
gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
|
|
84
|
+
gl.stencilMask(0xff);
|
|
85
|
+
gl.clear(gl.STENCIL_BUFFER_BIT);
|
|
86
|
+
// matrix
|
|
87
|
+
twgl.setUniforms(this.maskProgramInfo, { u_matrix: matrix });
|
|
88
|
+
// pos & indices
|
|
89
|
+
twgl.setBuffersAndAttributes(gl, this.maskProgramInfo, this.maskBufferInfo);
|
|
90
|
+
// draw
|
|
91
|
+
var elementType = gl.UNSIGNED_SHORT;
|
|
92
|
+
if (this.maskBufferInfo.numElements / 3 > 65535) {
|
|
93
|
+
// 使 drawElements 支持 UNSIGNED_INT 类型
|
|
94
|
+
gl.getExtension('OES_element_index_uint');
|
|
95
|
+
elementType = gl.UNSIGNED_INT;
|
|
96
|
+
}
|
|
97
|
+
gl.drawElements(gl.TRIANGLES, this.maskBufferInfo.numElements, elementType, 0);
|
|
98
|
+
}
|
|
99
|
+
// texture program
|
|
100
|
+
gl.useProgram(this.programInfo.program);
|
|
101
|
+
if ((_a = this.maskProgramInfo) === null || _a === void 0 ? void 0 : _a.program) {
|
|
102
|
+
// stencil test
|
|
103
|
+
var ref = this.maskProperty.type === 'out' ? 0 : 1;
|
|
104
|
+
gl.stencilFunc(gl.EQUAL, ref, 0xff);
|
|
105
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
|
|
106
|
+
}
|
|
107
|
+
// uniforms
|
|
108
|
+
twgl.setUniforms(this.programInfo, {
|
|
109
|
+
u_matrix: matrix,
|
|
110
|
+
u_opacity: (_b = this.option.opacity) !== null && _b !== void 0 ? _b : 1,
|
|
111
|
+
u_sampler: this.texture,
|
|
112
|
+
});
|
|
113
|
+
// pos, uv & indices
|
|
114
|
+
twgl.setBuffersAndAttributes(gl, this.programInfo, this.bufferInfo);
|
|
115
|
+
// draw
|
|
116
|
+
gl.drawElements(gl.TRIANGLES, this.arrugado.trigs.length, gl.UNSIGNED_SHORT, 0);
|
|
117
|
+
gl.clear(gl.STENCIL_BUFFER_BIT);
|
|
118
|
+
gl.disable(gl.STENCIL_TEST);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Updates the URL, the projection, the coordinates, the opacity or the resampling of the image.
|
|
123
|
+
* @param {Object} option Options object.
|
|
124
|
+
* @param {string} [option.url] Image URL.
|
|
125
|
+
* @param {string} [option.projection] Projection with EPSG code that points to the image..
|
|
126
|
+
* @param {Array<Array<number>>} [option.coordinates] Four geographical coordinates,
|
|
127
|
+
* @param {number} [option.opacity] opacity of the image.
|
|
128
|
+
* @param {string} [option.resampling] The resampling/interpolation method to use for overscaling.
|
|
129
|
+
*/
|
|
130
|
+
ImageLayer.prototype.updateImage = function (option) {
|
|
131
|
+
var _a, _b, _c, _d, _e;
|
|
132
|
+
if (this.gl && this.map) {
|
|
133
|
+
this.option.opacity = (_a = option.opacity) !== null && _a !== void 0 ? _a : this.option.opacity;
|
|
134
|
+
if (option.projection || option.coordinates) {
|
|
135
|
+
this.option.projection = (_b = option.projection) !== null && _b !== void 0 ? _b : this.option.projection;
|
|
136
|
+
this.option.coordinates = (_c = option.coordinates) !== null && _c !== void 0 ? _c : this.option.coordinates;
|
|
137
|
+
// reinit arrugator
|
|
138
|
+
this.arrugado = initArrugator(this.option.projection, this.option.coordinates, this.option.arrugatorStep);
|
|
139
|
+
this.bufferInfo = twgl.createBufferInfoFromArrays(this.gl, {
|
|
140
|
+
a_pos: { numComponents: 2, data: this.arrugado.pos },
|
|
141
|
+
a_uv: { numComponents: 2, data: this.arrugado.uv },
|
|
142
|
+
indices: this.arrugado.trigs,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (option.url || option.resampling) {
|
|
146
|
+
this.loaded = false;
|
|
147
|
+
this.option.url = (_d = option.url) !== null && _d !== void 0 ? _d : this.option.url;
|
|
148
|
+
this.option.resampling = (_e = option.resampling) !== null && _e !== void 0 ? _e : this.option.resampling;
|
|
149
|
+
// reload image
|
|
150
|
+
this.loadTexture(this.map, this.gl);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
this.map.triggerRepaint();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return this;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Updates the mask property
|
|
160
|
+
* @param {MaskProperty} mask The mask property.
|
|
161
|
+
*/
|
|
162
|
+
ImageLayer.prototype.updateMask = function (mask) {
|
|
163
|
+
if (this.gl && this.map) {
|
|
164
|
+
if (mask.data) {
|
|
165
|
+
if (!this.maskProgramInfo) {
|
|
166
|
+
this.maskProgramInfo = twgl.createProgramInfo(this.gl, [maskvs, maskfs]);
|
|
167
|
+
}
|
|
168
|
+
this.maskProperty = Object.assign(this.maskProperty, mask);
|
|
169
|
+
this.maskBufferInfo = this.getMaskBufferInfo(this.gl, this.maskProperty.data);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
this.maskProgramInfo && this.gl.deleteProgram(this.maskProgramInfo.program);
|
|
173
|
+
this.maskProgramInfo = undefined;
|
|
174
|
+
this.maskBufferInfo = undefined;
|
|
175
|
+
}
|
|
176
|
+
this.map.triggerRepaint();
|
|
177
|
+
}
|
|
178
|
+
return this;
|
|
179
|
+
};
|
|
180
|
+
ImageLayer.prototype.loadTexture = function (map, gl) {
|
|
181
|
+
var _this = this;
|
|
182
|
+
// 创建纹理
|
|
183
|
+
var filter = this.option.resampling === 'nearest' ? gl.NEAREST : gl.LINEAR;
|
|
184
|
+
this.texture = twgl.createTexture(gl, {
|
|
185
|
+
src: this.option.url,
|
|
186
|
+
crossOrigin: this.option.crossOrigin,
|
|
187
|
+
minMag: filter,
|
|
188
|
+
flipY: 0,
|
|
189
|
+
}, function () {
|
|
190
|
+
_this.loaded = true;
|
|
191
|
+
map.triggerRepaint();
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
ImageLayer.prototype.getMaskBufferInfo = function (gl, data) {
|
|
195
|
+
var positions = [];
|
|
196
|
+
var triangles = [];
|
|
197
|
+
if (data.type === 'MultiPolygon') {
|
|
198
|
+
// type: 'MultiPolygon'
|
|
199
|
+
var polyCount = data.coordinates.length;
|
|
200
|
+
var triangleStartIndex_1 = 0;
|
|
201
|
+
for (var i = 0; i < polyCount; i++) {
|
|
202
|
+
var coordinates = data.coordinates[i];
|
|
203
|
+
var flattened = flatten(coordinates);
|
|
204
|
+
var vertices = flattened.vertices, holes = flattened.holes, dimensions = flattened.dimensions;
|
|
205
|
+
var triangle = earcut(vertices, holes, dimensions);
|
|
206
|
+
var triangleNew = triangle.map(function (item) { return item + triangleStartIndex_1; });
|
|
207
|
+
triangleStartIndex_1 += vertices.length / 2;
|
|
208
|
+
// positions.push(...vertices)
|
|
209
|
+
// triangles.push(...triangleNew)
|
|
210
|
+
for (var m = 0; m < vertices.length; m++) {
|
|
211
|
+
positions.push(vertices[m]);
|
|
212
|
+
}
|
|
213
|
+
for (var n = 0; n < triangleNew.length; n++) {
|
|
214
|
+
triangles.push(triangleNew[n]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// type: 'Polygon'
|
|
220
|
+
var flattened = flatten(data.coordinates);
|
|
221
|
+
var vertices = flattened.vertices, holes = flattened.holes, dimensions = flattened.dimensions;
|
|
222
|
+
positions = vertices;
|
|
223
|
+
triangles = earcut(vertices, holes, dimensions);
|
|
224
|
+
}
|
|
225
|
+
return twgl.createBufferInfoFromArrays(gl, {
|
|
226
|
+
a_pos: { numComponents: 2, data: positions },
|
|
227
|
+
indices: triangles.length / 3 > 65535 ? new Uint32Array(triangles) : new Uint16Array(triangles),
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
return ImageLayer;
|
|
231
|
+
}());
|
|
232
|
+
export default ImageLayer;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/* eslint-disable guard-for-in */
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import TinyQueue from 'tinyqueue';
|
|
4
|
+
var Arrugator = /** @class */ (function () {
|
|
5
|
+
function Arrugator(projector, verts, uv, trigs) {
|
|
6
|
+
this._stepsWithSameEpsilon = 0;
|
|
7
|
+
// The projector function. Must be able to take
|
|
8
|
+
// an array of two numbers [x,y] and return an array of
|
|
9
|
+
// two numbers.
|
|
10
|
+
// The typical use case is a proj4(from,to).forward function.
|
|
11
|
+
this._projector = projector;
|
|
12
|
+
// A two-dimensional array of vertex coordinates. Each vertex is a
|
|
13
|
+
// two-element [x,y] array.
|
|
14
|
+
this._verts = verts;
|
|
15
|
+
// A two-dimensional array of UV-map coordinates. These are intended to
|
|
16
|
+
// represent the [0,0]-[1-1] coordinate space of WebGL textures. Each
|
|
17
|
+
// n-th element is the UV coordinates of the n-th vertex. These shall
|
|
18
|
+
// be linearly interpolated when splitting segments.
|
|
19
|
+
this._uv = uv;
|
|
20
|
+
// A two-dimensional array of vertex coordinates, projected. Each
|
|
21
|
+
// vertex is a two-element [x,y] array.
|
|
22
|
+
this._projVerts = verts.map(projector);
|
|
23
|
+
// A two-dimensional array of triangle vertex IDs. Each triangle is a
|
|
24
|
+
// three-element [v1,v2,v3] array.
|
|
25
|
+
// The mesh is **expected** to be compact, planar, non-overlapping.
|
|
26
|
+
this._trigs = trigs;
|
|
27
|
+
// A map of segments to vertices. Key is the segment index (generated inside
|
|
28
|
+
// arrugator), value is an array of two vertex indices.
|
|
29
|
+
this._segs = [];
|
|
30
|
+
this._segCount = 0;
|
|
31
|
+
// A map of segments to triangles. Key is the segment index (generated inside
|
|
32
|
+
// arrugator), value is an array of triangle indices (all segments should
|
|
33
|
+
// have either 1 or 2 triangles associated)
|
|
34
|
+
this._segTrigs = [];
|
|
35
|
+
// A priority queue of segments, ordered by their epsilons, in descending order.
|
|
36
|
+
this._queue = new TinyQueue([], function (a, b) {
|
|
37
|
+
return b.epsilon - a.epsilon;
|
|
38
|
+
});
|
|
39
|
+
// A map of vertex indices to segment indices.
|
|
40
|
+
this._vertToSeg = new Array(verts.length);
|
|
41
|
+
for (var i in this._verts) {
|
|
42
|
+
this._vertToSeg[i] = [];
|
|
43
|
+
}
|
|
44
|
+
/// NOTE: Not using .fill([]), because that would use a reference to the *same*
|
|
45
|
+
/// empty array for every element.
|
|
46
|
+
for (var t in this._trigs) {
|
|
47
|
+
var trig = this._trigs[t];
|
|
48
|
+
var v0 = trig[0];
|
|
49
|
+
var v1 = trig[1];
|
|
50
|
+
var v2 = trig[2];
|
|
51
|
+
this._segment(v0, v1, t);
|
|
52
|
+
this._segment(v1, v2, t);
|
|
53
|
+
this._segment(v2, v0, t);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Returns the segment index linking the two given vertex indices;
|
|
57
|
+
// Must be passed a triangle index to use as context.
|
|
58
|
+
// Might create a new segment index (as well as segment data structure and
|
|
59
|
+
// entry in the priority queue).
|
|
60
|
+
Arrugator.prototype._segment = function (v1, v2, t, maxEpsilon) {
|
|
61
|
+
if (maxEpsilon === void 0) { maxEpsilon = Infinity; }
|
|
62
|
+
if (this._vertToSeg[v1] && this._vertToSeg[v1][v2] !== undefined) {
|
|
63
|
+
var found = this._vertToSeg[v1][v2];
|
|
64
|
+
if (!this._segTrigs[found].includes(t)) {
|
|
65
|
+
this._segTrigs[found].push(t);
|
|
66
|
+
}
|
|
67
|
+
return found;
|
|
68
|
+
}
|
|
69
|
+
var segIdx = this._segCount++;
|
|
70
|
+
this._segs[segIdx] = [v1, v2];
|
|
71
|
+
this._vertToSeg[v1][v2] = segIdx;
|
|
72
|
+
this._vertToSeg[v2][v1] = segIdx;
|
|
73
|
+
this._segTrigs[segIdx] = [t];
|
|
74
|
+
// Calculate segment epsilon
|
|
75
|
+
// The "epsilon" of a segment is the square of the midpoint projection distance:
|
|
76
|
+
// i.e. the square of the distance between:
|
|
77
|
+
// - the projected midpoint of the two vertices, and
|
|
78
|
+
// - the midpoint of the two projected vertices,
|
|
79
|
+
// the distance function being euclidean distance in the "destination"
|
|
80
|
+
// projection, squared.
|
|
81
|
+
var midpoint = [
|
|
82
|
+
(this._verts[v1][0] + this._verts[v2][0]) / 2,
|
|
83
|
+
(this._verts[v1][1] + this._verts[v2][1]) / 2,
|
|
84
|
+
];
|
|
85
|
+
var projectedMid = this._projector(midpoint);
|
|
86
|
+
var midProjected = [
|
|
87
|
+
(this._projVerts[v1][0] + this._projVerts[v2][0]) / 2,
|
|
88
|
+
(this._projVerts[v1][1] + this._projVerts[v2][1]) / 2,
|
|
89
|
+
];
|
|
90
|
+
var epsilon = Math.pow((projectedMid[0] - midProjected[0]), 2) + Math.pow((projectedMid[1] - midProjected[1]), 2);
|
|
91
|
+
if (Number.isFinite(epsilon) && epsilon < maxEpsilon) {
|
|
92
|
+
this._queue.push({
|
|
93
|
+
v1: v1,
|
|
94
|
+
v2: v2,
|
|
95
|
+
epsilon: epsilon,
|
|
96
|
+
midpoint: midpoint,
|
|
97
|
+
projectedMid: projectedMid,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return segIdx;
|
|
101
|
+
};
|
|
102
|
+
// Outputs shallow copies of some data structures at the current step.
|
|
103
|
+
Arrugator.prototype.output = function () {
|
|
104
|
+
// Most data structs are 2-dimensional arrays, and doing a shallow copy
|
|
105
|
+
// of the first level *should* just work.
|
|
106
|
+
return {
|
|
107
|
+
unprojected: Array.from(this._verts),
|
|
108
|
+
projected: Array.from(this._projVerts),
|
|
109
|
+
uv: Array.from(this._uv),
|
|
110
|
+
trigs: Array.from(this._trigs),
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
// Subdivides the mesh until the maximum segment epsilon is below the
|
|
114
|
+
// given threshold.
|
|
115
|
+
// The `targetEpsilon` parameter must be in the same units as the
|
|
116
|
+
// internal epsilons: units of the projected CRS, **squared**.
|
|
117
|
+
Arrugator.prototype.lowerEpsilon = function (targetEpsilon) {
|
|
118
|
+
var currentEpsilon = this._queue.peek().epsilon;
|
|
119
|
+
var lastEpsilon = currentEpsilon;
|
|
120
|
+
while (currentEpsilon >= targetEpsilon) {
|
|
121
|
+
this.step();
|
|
122
|
+
currentEpsilon = this._queue.peek().epsilon;
|
|
123
|
+
if (currentEpsilon === lastEpsilon) {
|
|
124
|
+
this._stepsWithSameEpsilon++;
|
|
125
|
+
if (this._stepsWithSameEpsilon < 500) {
|
|
126
|
+
console.warn('Arrugator stopped due to epsilon stall. Raster may need hints for proper arrugation.');
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this._stepsWithSameEpsilon = 0;
|
|
132
|
+
lastEpsilon = currentEpsilon;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
Object.defineProperty(Arrugator.prototype, "epsilon", {
|
|
137
|
+
get: function () {
|
|
138
|
+
return this._queue.peek().epsilon;
|
|
139
|
+
},
|
|
140
|
+
set: function (ep) {
|
|
141
|
+
return this.lowerEpsilon(ep);
|
|
142
|
+
},
|
|
143
|
+
enumerable: false,
|
|
144
|
+
configurable: true
|
|
145
|
+
});
|
|
146
|
+
// Triggers subdivision of the segment with the largest epsilon.
|
|
147
|
+
Arrugator.prototype.step = function () {
|
|
148
|
+
var seg = this._queue.pop();
|
|
149
|
+
return this._splitSegment(seg, seg.epsilon);
|
|
150
|
+
};
|
|
151
|
+
// Triggers *one* subdivision of *all* segments in the queue.
|
|
152
|
+
// Can be useful to run this prior to stepping, in order to overcome
|
|
153
|
+
// artefacts
|
|
154
|
+
Arrugator.prototype.force = function () {
|
|
155
|
+
var _this = this;
|
|
156
|
+
var segments = this._queue.data;
|
|
157
|
+
this._queue.data = []; // Empties the queue
|
|
158
|
+
this._queue.length = 0;
|
|
159
|
+
segments.forEach(function (seg) { return _this._splitSegment(seg, Infinity); });
|
|
160
|
+
};
|
|
161
|
+
// Splits the given segment.
|
|
162
|
+
// This deletes the segment, spawns a new vertex at the midpoint, and
|
|
163
|
+
// for each triangle the segment was originally a part of (either 1 or 2),
|
|
164
|
+
// the triangle is divided into two.
|
|
165
|
+
Arrugator.prototype._splitSegment = function (seg, maxEpsilon) {
|
|
166
|
+
// Which are the two vertices affected by the popped segment?
|
|
167
|
+
var v1 = seg.v1;
|
|
168
|
+
var v2 = seg.v2;
|
|
169
|
+
var s = this._vertToSeg[v1] && this._vertToSeg[v1][v2];
|
|
170
|
+
// Which triangle(s) are affected by the popped segment?
|
|
171
|
+
var trigs = this._segTrigs[s];
|
|
172
|
+
// Sanity check
|
|
173
|
+
if (trigs.length >= 3) {
|
|
174
|
+
throw new Error('Somehow a segment is shared by three triangles');
|
|
175
|
+
}
|
|
176
|
+
// Clean up refs
|
|
177
|
+
delete this._segTrigs[s];
|
|
178
|
+
delete this._segs[s];
|
|
179
|
+
delete this._vertToSeg[v1][v2];
|
|
180
|
+
delete this._vertToSeg[v2][v1];
|
|
181
|
+
// What is the vertex ID of the new midpoint vertex?
|
|
182
|
+
var vm = this._verts.length;
|
|
183
|
+
this._projVerts[vm] = seg.projectedMid;
|
|
184
|
+
this._verts[vm] = seg.midpoint;
|
|
185
|
+
this._vertToSeg[vm] = [];
|
|
186
|
+
this._uv[vm] = [
|
|
187
|
+
(this._uv[v1][0] + this._uv[v2][0]) / 2,
|
|
188
|
+
(this._uv[v1][1] + this._uv[v2][1]) / 2,
|
|
189
|
+
];
|
|
190
|
+
for (var _i = 0, trigs_1 = trigs; _i < trigs_1.length; _i++) {
|
|
191
|
+
var t = trigs_1[_i];
|
|
192
|
+
this._splitTriangle(v1, v2, vm, t, maxEpsilon);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
// Split a triangle in two.
|
|
196
|
+
// Must be given vertex indices of the segment being splitted, the index of the new
|
|
197
|
+
// midpoint vertex, and the triangle index.
|
|
198
|
+
// Shall silently drop any new segments with an epsilon larger than the
|
|
199
|
+
// given one. This means that the segment shall be in the triangle mesh,
|
|
200
|
+
// but will not be queued and therefore not subdivided ever.
|
|
201
|
+
Arrugator.prototype._splitTriangle = function (v1, v2, vm, t, epsilon) {
|
|
202
|
+
if (epsilon === void 0) { epsilon = Infinity; }
|
|
203
|
+
var tvs = this._trigs[t];
|
|
204
|
+
var v3;
|
|
205
|
+
var winding = false;
|
|
206
|
+
// Fetch the ID of the 3rd vertex in the original triangle, and the winding order
|
|
207
|
+
if (tvs[0] === v1 && tvs[1] === v2) {
|
|
208
|
+
v3 = tvs[2];
|
|
209
|
+
winding = true; // A-B-C
|
|
210
|
+
}
|
|
211
|
+
else if (tvs[1] === v1 && tvs[2] === v2) {
|
|
212
|
+
v3 = tvs[0];
|
|
213
|
+
winding = true; // C-A-B
|
|
214
|
+
}
|
|
215
|
+
else if (tvs[2] === v1 && tvs[0] === v2) {
|
|
216
|
+
v3 = tvs[1];
|
|
217
|
+
winding = true; // B-C-A
|
|
218
|
+
}
|
|
219
|
+
else if (tvs[1] === v1 && tvs[0] === v2) {
|
|
220
|
+
v3 = tvs[2];
|
|
221
|
+
winding = false; // B-A-C
|
|
222
|
+
}
|
|
223
|
+
else if (tvs[2] === v1 && tvs[1] === v2) {
|
|
224
|
+
v3 = tvs[0];
|
|
225
|
+
winding = false; // C-B-A
|
|
226
|
+
}
|
|
227
|
+
else if (tvs[0] === v1 && tvs[2] === v2) {
|
|
228
|
+
v3 = tvs[1];
|
|
229
|
+
winding = false; // A-C-B
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
throw new Error('Data structure mishap: could not fetch 3rd vertex used in triangle');
|
|
233
|
+
}
|
|
234
|
+
// Index of the first "half" triangle will be the reused index of the original triangle
|
|
235
|
+
// Index of the second "half" triangle must be allocated at the end of the triangles structure
|
|
236
|
+
var t2 = this._trigs.length;
|
|
237
|
+
if (winding) {
|
|
238
|
+
this._trigs[t] = [v1, vm, v3];
|
|
239
|
+
this._trigs[t2] = [vm, v2, v3];
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
this._trigs[t] = [vm, v1, v3];
|
|
243
|
+
this._trigs[t2] = [v2, vm, v3];
|
|
244
|
+
}
|
|
245
|
+
// Clean up references from old segments
|
|
246
|
+
var s1 = this._vertToSeg[v1] && this._vertToSeg[v1][v2];
|
|
247
|
+
var s2 = this._vertToSeg[v2] && this._vertToSeg[v2][v3];
|
|
248
|
+
var s3 = this._vertToSeg[v3] && this._vertToSeg[v3][v1];
|
|
249
|
+
function filterTrig(i) {
|
|
250
|
+
return i !== t;
|
|
251
|
+
}
|
|
252
|
+
if (s1 !== undefined) {
|
|
253
|
+
this._segTrigs[s1] = this._segTrigs[s1].filter(filterTrig);
|
|
254
|
+
}
|
|
255
|
+
if (s2 !== undefined) {
|
|
256
|
+
this._segTrigs[s2] = this._segTrigs[s2].filter(filterTrig);
|
|
257
|
+
}
|
|
258
|
+
if (s3 !== undefined) {
|
|
259
|
+
this._segTrigs[s3] = this._segTrigs[s3].filter(filterTrig);
|
|
260
|
+
}
|
|
261
|
+
this._segment(v1, vm, t, epsilon);
|
|
262
|
+
this._segment(vm, v3, t, epsilon);
|
|
263
|
+
this._segment(v3, v1, t, epsilon);
|
|
264
|
+
this._segment(v2, vm, t2, epsilon);
|
|
265
|
+
this._segment(vm, v3, t2, epsilon);
|
|
266
|
+
this._segment(v3, v2, t2, epsilon);
|
|
267
|
+
};
|
|
268
|
+
return Arrugator;
|
|
269
|
+
}());
|
|
270
|
+
export default Arrugator;
|
|
@@ -1,39 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
import Arrugator from '
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// 墨卡托投影的左上角坐标,对应 mapbox 左上角起始坐标 [0,0]
|
|
6
|
-
var origin = [-20037508.342789244, 20037508.342789244];
|
|
7
|
-
// 坐标转换为 Arrugator 坐标 top-left, top-left, top-left, top-left)
|
|
8
|
-
var verts = [coordinates[0], coordinates[3], coordinates[1], coordinates[2]];
|
|
9
|
-
// 转换为 EPSG:3857
|
|
10
|
-
var projector = proj4(fromProj, 'EPSG:3857').forward;
|
|
11
|
-
// 改写坐标转换函数,因为 mapbox 的墨卡托坐标是 0-1,并且对应地理范围与标准 3857 不同
|
|
12
|
-
function forward(coors) {
|
|
13
|
-
// 墨卡托坐标
|
|
14
|
-
var coor_3857 = projector(coors);
|
|
15
|
-
// 墨卡托坐标转换到 0-1 区间,origin 对应 mapbox 0 0点
|
|
16
|
-
var mapbox_coor1 = Math.abs((coor_3857[0] - origin[0]) / (20037508.342789244 * 2));
|
|
17
|
-
var mapbox_coor2 = Math.abs((coor_3857[1] - origin[1]) / (20037508.342789244 * 2));
|
|
18
|
-
return [mapbox_coor1, mapbox_coor2];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
[0,
|
|
24
|
-
[
|
|
25
|
-
[1,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
[0,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
import proj4 from 'proj4';
|
|
2
|
+
import Arrugator from './Arrugator';
|
|
3
|
+
export function initArrugator(fromProj, coordinates, step) {
|
|
4
|
+
if (step === void 0) { step = 100; }
|
|
5
|
+
// 墨卡托投影的左上角坐标,对应 mapbox 左上角起始坐标 [0,0]
|
|
6
|
+
var origin = [-20037508.342789244, 20037508.342789244];
|
|
7
|
+
// 坐标转换为 Arrugator 坐标 top-left, top-left, top-left, top-left)
|
|
8
|
+
var verts = [coordinates[0], coordinates[3], coordinates[1], coordinates[2]];
|
|
9
|
+
// 转换为 EPSG:3857
|
|
10
|
+
var projector = proj4(fromProj, 'EPSG:3857').forward;
|
|
11
|
+
// 改写坐标转换函数,因为 mapbox 的墨卡托坐标是 0-1,并且对应地理范围与标准 3857 不同
|
|
12
|
+
function forward(coors) {
|
|
13
|
+
// 墨卡托坐标
|
|
14
|
+
var coor_3857 = projector(coors);
|
|
15
|
+
// 墨卡托坐标转换到 0-1 区间,origin 对应 mapbox 0 0点
|
|
16
|
+
var mapbox_coor1 = Math.abs((coor_3857[0] - origin[0]) / (20037508.342789244 * 2));
|
|
17
|
+
var mapbox_coor2 = Math.abs((coor_3857[1] - origin[1]) / (20037508.342789244 * 2));
|
|
18
|
+
return [mapbox_coor1, mapbox_coor2];
|
|
19
|
+
}
|
|
20
|
+
// 纹理uv坐标
|
|
21
|
+
var sourceUV = [
|
|
22
|
+
[0, 0], // top-left
|
|
23
|
+
[0, 1], // bottom-left
|
|
24
|
+
[1, 0], // top-right
|
|
25
|
+
[1, 1], // bottom-right
|
|
26
|
+
];
|
|
27
|
+
var arrugator = new Arrugator(forward, verts, sourceUV, [
|
|
28
|
+
[0, 1, 3],
|
|
29
|
+
[0, 3, 2],
|
|
30
|
+
]);
|
|
31
|
+
if (step > 0) {
|
|
32
|
+
arrugator.force();
|
|
33
|
+
for (var i = 0; i < step; i++) {
|
|
34
|
+
arrugator.step();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
var arrugado = arrugator.output();
|
|
38
|
+
return {
|
|
39
|
+
pos: arrugado.projected.flat(), // mapbox 墨卡托坐标
|
|
40
|
+
uv: arrugado.uv.flat(), // uv 纹理
|
|
41
|
+
trigs: arrugado.trigs.flat(), // 三角形索引
|
|
42
|
+
};
|
|
43
|
+
}
|