-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.ts
410 lines (388 loc) · 10.9 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
/**
* Provides a Mapbox custom layer that renders a simple circle.
*
* @remarks
*
* A circle is represented with the center and radius in meters (not pixels).
*
* @packageDocumentation
*/
import {
type CustomLayerInterface,
type Map,
MercatorCoordinate,
} from 'mapbox-gl';
import { loadShader } from './private/load-shader';
import type { LngLat, RGBA } from './types';
export { LngLat, RGBA } from './types';
/**
* Default radius of a circle.
*
* @beta
*/
export const DEFAULT_RADIUS_IN_METERS = 50;
/**
* Default center of a circle (Tokyo Station).
*
* @beta
*/
export const DEFAULT_CENTER = { lng: 139.7671, lat: 35.6812 } as const;
/**
* Default fill color of a circle (opaque white).
*
* @beta
*/
export const DEFAULT_FILL = {
red: 1.0,
green: 1.0,
blue: 1.0,
alpha: 1.0,
} as const;
/**
* Default number of triangles to approximate a circle.
*
* @beta
*/
export const DEFAULT_NUM_TRIANGLES = 32;
/**
* Constructor properties for `GeoCircleLayer`.
*
* @beta
*/
export interface GeoCircleLayerProperties {
/** Radius of the circle in meters. */
radiusInMeters?: number;
/** Center of the circle. */
center?: LngLat;
/** Fill color of the circle. */
fill?: RGBA;
/** Number of triangles to approximate the circle. */
numTriangles?: number;
}
/**
* Custom layer that renders a simple circle.
*
* @beta
*/
export class GeoCircleLayer implements CustomLayerInterface {
/** Radius of the circle. */
private _radiusInMeters: number;
/** Center of the circle. */
private _center: LngLat;
/** Fill color of the circle. */
private _fill: RGBA;
/** Fill color for blending. Alpha is multiplied to the other components. */
private _fillForBlending: RGBA;
/** Number of triangles to approximate the circle. */
private _numTriangles: number;
/** Current map instance. */
private map: Map | null = null;
/** Function that removes listeners from `map`. */
private removeListeners: (() => void) | null = null;
/** Vertex shader. */
private vertexShader: WebGLShader | null = null;
/** Fragment shader. */
private fragmentShader: WebGLShader | null = null;
/** Shader program. */
private program: WebGLProgram | null = null;
/** Position attribute index. */
private aPos: GLint | undefined = undefined;
/** Buffer. */
private buffer: WebGLBuffer | null = null;
/** Whether the buffer needs refresh. */
private isDirty: boolean = true;
/**
* Initializes a layer.
*
* @remarks
*
* You may omit all or part of `props`.
* The following are default values for the properties,
* - `radiusInMeters`: `50`
* - `center`: `{ lng: 139.7671, lat: 35.6812 }` (Tokyo Station)
* - `fill`: `{ red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0 }` (white)
* - `numTriangles`: `32`
*
* Since v0.2.0, you no longer have to premultiply the alpha to the red,
* gree, and blue components of `fill`.
*
* @param id -
*
* ID of the layer.
*
* @param props -
*
* Properties of the circle.
*
* @throws RangeError
*
* If `props.radiusInMeters` is negative,
* or if `props.numTriangles` is less than `3`.
*/
constructor(
public readonly id: string,
props?: GeoCircleLayerProperties,
) {
const radiusInMeters = props?.radiusInMeters ?? DEFAULT_RADIUS_IN_METERS;
if (radiusInMeters < 0) {
throw new RangeError(
`radiusInMeters must be ≥ 0 but ${radiusInMeters} was given`,
);
}
const numTriangles = props?.numTriangles ?? DEFAULT_NUM_TRIANGLES;
if (numTriangles < 3) {
throw new RangeError(
`numTriangles must be ≥ 3 but ${numTriangles} was given`,
);
}
this._radiusInMeters = radiusInMeters;
this._center = props?.center ?? DEFAULT_CENTER;
this._fill = props?.fill ?? DEFAULT_FILL;
this._fillForBlending = multiplyAlpha(this._fill);
this._numTriangles = numTriangles;
}
/** Type is always "custom". */
get type(): 'custom' {
return 'custom';
}
/**
* Radius in meters of the circle.
*
* @remarks
*
* Updating this property will trigger repaint of the map.
*
* Throws `RangeError`, if a negative value is given to the setter.
*/
get radiusInMeters(): number {
return this._radiusInMeters;
}
set radiusInMeters(radiusInMeters: number) {
if (radiusInMeters < 0) {
throw new RangeError(
`radiusInMeters must be ≥ 0 but ${radiusInMeters} was given`,
);
}
this._radiusInMeters = radiusInMeters;
this.triggerRepaint();
}
/**
* Center of the circle.
*
* @remarks
*
* Updating this property will trigger repaint of the map.
*/
get center(): LngLat {
return this._center;
}
set center(center: LngLat) {
this._center = center;
this.triggerRepaint();
}
/**
* Fill color of the circle.
*
* @remarks
*
* Since v0.2.0, you no longer have to premultiply the alpha to the red,
* green, and blue components.
*
* Updating this property will trigger repaint of the map.
*/
get fill(): RGBA {
return this._fill;
}
set fill(fill: RGBA) {
this._fill = fill;
this._fillForBlending = multiplyAlpha(fill);
// no need to recalculate the circle
this.map?.triggerRepaint();
}
/**
* Number of triangles to approximate the circle.
*
* @remarks
*
* Updating this property will trigger repaint of the map.
*
* Throws `RangeError`, if a value less than 3 is given to the setter.
*/
get numTriangles(): number {
return this._numTriangles;
}
set numTriangles(numTriangles: number) {
if (numTriangles < 3) {
throw new RangeError(
`numTriangles must be ≥ 3 but ${numTriangles} was given`,
);
}
this._numTriangles = numTriangles;
this.triggerRepaint();
}
/** Requests repaint. */
private triggerRepaint() {
this.isDirty = true;
this.map?.triggerRepaint();
}
onAdd(map: Map, gl: WebGLRenderingContext) {
this.map = map;
// initialization of WebGL objects is also necessary when the WebGL context
// is lost and restored:
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
//
// so reuses the following function
const createWebGLObjects = () => {
const vertexSource = `
uniform mat4 u_matrix;
attribute vec2 a_pos;
void main() {
gl_Position = u_matrix * vec4(a_pos, 0.0, 1.0);
}
`.trim();
const fragmentSource = `
uniform lowp vec4 u_fill;
void main() {
/* premultiplies the alpha to the RGB components. */
gl_FragColor = u_fill;
}
`.trim();
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexSource);
this.vertexShader = vertexShader;
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
this.fragmentShader = fragmentShader;
const program = gl.createProgram()!;
// everything should work even if program is null
this.program = program;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(
'failed to link a program',
gl.getProgramInfoLog(program),
);
throw new Error(
`failed to link a program: ${gl.getProgramInfoLog(program)}`,
);
}
const aPos = gl.getAttribLocation(program, 'a_pos');
this.aPos = aPos;
const buffer = gl.createBuffer();
this.buffer = buffer;
};
createWebGLObjects();
// processes WebGL context events
const onWebglcontextlost = () => {
this.clearWebGLReferences();
};
const onWebglcontextrestored = () => {
// according to the MDN documentation, the WebGL context object associated
// with the same canvas is always the same.
// so it should be safe to reference `gl` here.
createWebGLObjects();
};
map.on('webglcontextlost', onWebglcontextlost);
map.on('webglcontextrestored', onWebglcontextrestored);
this.removeListeners = () => {
map.off('webglcontextlost', onWebglcontextlost);
map.off('webglcontextrestored', onWebglcontextrestored);
};
}
onRemove(map: Map, gl: WebGLRenderingContext) {
if (this.removeListeners != null) {
this.removeListeners();
}
gl.deleteBuffer(this.buffer);
gl.deleteProgram(this.program);
gl.deleteShader(this.vertexShader);
gl.deleteShader(this.fragmentShader);
this.clearWebGLReferences();
this.map = null;
this.isDirty = true;
}
private clearWebGLReferences() {
this.buffer = null;
this.program = null;
this.vertexShader = null;
this.fragmentShader = null;
}
prerender(gl: WebGLRenderingContext) {
// refreshes the buffer if necessary
if (this.isDirty) {
const buffer = this.buffer;
if (buffer == null) {
console.error('buffer is not ready');
return;
}
this.isDirty = false;
const center = MercatorCoordinate.fromLngLat(this._center);
const radius =
this._radiusInMeters * center.meterInMercatorCoordinateUnits();
const points = [center.x, center.y];
for (let i = 0; i < this._numTriangles; ++i) {
const angle = 2 * Math.PI * (i / this._numTriangles);
const x = center.x + radius * Math.cos(angle);
const y = center.y + radius * Math.sin(angle);
points.push(x);
points.push(y);
}
points.push(center.x + radius);
points.push(center.y);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(points),
gl.DYNAMIC_DRAW,
);
}
}
render(gl: WebGLRenderingContext, matrix: number[]) {
const program = this.program;
const aPos = this.aPos;
const buffer = this.buffer;
if (program == null || aPos == null || buffer == null) {
console.error('shader is not ready');
return;
}
gl.useProgram(program);
gl.uniformMatrix4fv(
gl.getUniformLocation(program, 'u_matrix'),
false,
matrix,
);
const { red, green, blue, alpha } = this._fillForBlending;
gl.uniform4fv(
gl.getUniformLocation(program, 'u_fill'),
[red, green, blue, alpha],
);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
// we can assume BLEND is enabled and the blendFunc is
// gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.drawArrays(gl.TRIANGLE_FAN, 0, this._numTriangles + 2);
}
}
/**
* Multiplies the alpha component to the other three components.
*
* @param color -
*
* Color to multiply the alpha.
*
* @returns
*
* New RGBA object that has the alpha multiplied to the other components of
* `color`.
*
* @beta
*/
export function multiplyAlpha({ red, green, blue, alpha }: RGBA): RGBA {
return {
red: red * alpha,
green: green * alpha,
blue: blue * alpha,
alpha,
};
}