@mcolabs/threebox-plugin 4.0.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/LICENSE.txt +97 -0
- package/README.md +199 -0
- package/dist/threebox.cjs +2 -0
- package/dist/threebox.cjs.map +1 -0
- package/dist/threebox.iife.js +2 -0
- package/dist/threebox.iife.js.map +1 -0
- package/dist/threebox.js +3521 -0
- package/dist/threebox.js.map +1 -0
- package/package.json +57 -0
- package/src/Threebox.js +1201 -0
- package/src/animation/AnimationManager.js +482 -0
- package/src/camera/CameraSync.js +298 -0
- package/src/index.js +8 -0
- package/src/objects/CSS2DRenderer.js +236 -0
- package/src/objects/LabelRenderer.js +70 -0
- package/src/objects/Object3D.js +32 -0
- package/src/objects/effects/BuildingShadows.js +163 -0
- package/src/objects/extrusion.js +59 -0
- package/src/objects/fflate.min.js +6 -0
- package/src/objects/label.js +25 -0
- package/src/objects/line.js +45 -0
- package/src/objects/loadObj.js +139 -0
- package/src/objects/objects.js +1111 -0
- package/src/objects/sphere.js +22 -0
- package/src/objects/tooltip.js +26 -0
- package/src/objects/tube.js +28 -0
- package/src/utils/ValueGenerator.js +11 -0
- package/src/utils/constants.js +23 -0
- package/src/utils/material.js +52 -0
- package/src/utils/suncalc.js +311 -0
- package/src/utils/utils.js +420 -0
- package/src/utils/validate.js +114 -0
package/src/Threebox.js
ADDED
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author peterqliu / https://github.com/peterqliu
|
|
3
|
+
* @author jscastro / https://github.com/jscastro76
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as THREE from 'three';
|
|
7
|
+
import CameraSync from './camera/CameraSync.js';
|
|
8
|
+
import utils from './utils/utils.js';
|
|
9
|
+
import SunCalc from './utils/suncalc.js';
|
|
10
|
+
import ThreeboxConstants from './utils/constants.js';
|
|
11
|
+
import Objects from './objects/objects.js';
|
|
12
|
+
import material from './utils/material.js';
|
|
13
|
+
import sphere from './objects/sphere.js';
|
|
14
|
+
import extrusion from './objects/extrusion.js';
|
|
15
|
+
import label from './objects/label.js';
|
|
16
|
+
import tooltip from './objects/tooltip.js';
|
|
17
|
+
import loader from './objects/loadObj.js';
|
|
18
|
+
import Object3D from './objects/Object3D.js';
|
|
19
|
+
import line from './objects/line.js';
|
|
20
|
+
import tube from './objects/tube.js';
|
|
21
|
+
import LabelRenderer from './objects/LabelRenderer.js';
|
|
22
|
+
import BuildingShadows from './objects/effects/BuildingShadows.js';
|
|
23
|
+
|
|
24
|
+
function Threebox(map, glContext, options){
|
|
25
|
+
|
|
26
|
+
this.init(map, glContext, options);
|
|
27
|
+
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
Threebox.prototype = {
|
|
31
|
+
|
|
32
|
+
repaint: function () {
|
|
33
|
+
this.map.repaint = true;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Threebox constructor init method
|
|
38
|
+
* @param {mapboxgl.map} map
|
|
39
|
+
* @param {WebGLRenderingContext} glContext
|
|
40
|
+
* @param {defaultOptions} options
|
|
41
|
+
*/
|
|
42
|
+
init: function (map, glContext, options) {
|
|
43
|
+
|
|
44
|
+
// apply starter options
|
|
45
|
+
this.options = utils._validate(options || {}, defaultOptions);
|
|
46
|
+
|
|
47
|
+
this.map = map;
|
|
48
|
+
this.map.tb = this; //[jscastro] needed if we want to queryRenderedFeatures from map.onload
|
|
49
|
+
|
|
50
|
+
this.objects = new Objects();
|
|
51
|
+
|
|
52
|
+
this.mapboxVersion = parseFloat(this.map.version);
|
|
53
|
+
|
|
54
|
+
// Set up a THREE.js scene
|
|
55
|
+
this.renderer = new THREE.WebGLRenderer({
|
|
56
|
+
alpha: true,
|
|
57
|
+
antialias: true,
|
|
58
|
+
preserveDrawingBuffer: options.preserveDrawingBuffer,
|
|
59
|
+
canvas: map.getCanvas(),
|
|
60
|
+
context: glContext
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
64
|
+
this.renderer.setSize(this.map.getCanvas().clientWidth, this.map.getCanvas().clientHeight);
|
|
65
|
+
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
66
|
+
this.renderer.autoClear = false;
|
|
67
|
+
|
|
68
|
+
// [jscastro] set labelRendered
|
|
69
|
+
this.labelRenderer = new LabelRenderer(this.map);
|
|
70
|
+
|
|
71
|
+
this.scene = new THREE.Scene();
|
|
72
|
+
this.world = new THREE.Group();
|
|
73
|
+
this.world.name = "world";
|
|
74
|
+
this.scene.add(this.world);
|
|
75
|
+
|
|
76
|
+
this.objectsCache = new Map();
|
|
77
|
+
this.zoomLayers = [];
|
|
78
|
+
|
|
79
|
+
this.fov = this.options.fov;
|
|
80
|
+
this.orthographic = this.options.orthographic || false;
|
|
81
|
+
|
|
82
|
+
//raycaster for mouse events
|
|
83
|
+
this.raycaster = new THREE.Raycaster();
|
|
84
|
+
this.raycaster.layers.set(0);
|
|
85
|
+
//this.raycaster.params.Points.threshold = 100;
|
|
86
|
+
|
|
87
|
+
this.mapCenter = this.map.getCenter();
|
|
88
|
+
this.mapCenterUnits = utils.projectToWorld([this.mapCenter.lng, this.mapCenter.lat]);
|
|
89
|
+
this.lightDateTime = new Date();
|
|
90
|
+
this.lightLng = this.mapCenter.lng;
|
|
91
|
+
this.lightLat = this.mapCenter.lat;
|
|
92
|
+
this.sunPosition;
|
|
93
|
+
this.rotationStep = 5;// degrees step size for rotation
|
|
94
|
+
this.gridStep = 6;// decimals to adjust the lnglat grid step, 6 = 11.1cm
|
|
95
|
+
this.altitudeStep = 0.1; // 1px = 0.1m = 10cm
|
|
96
|
+
this.defaultCursor = 'default';
|
|
97
|
+
|
|
98
|
+
this.lights = this.initLights;
|
|
99
|
+
if (this.options.defaultLights) this.defaultLights();
|
|
100
|
+
if (this.options.realSunlight) this.realSunlight(this.options.realSunlightHelper);
|
|
101
|
+
this.skyLayerName = 'sky-layer';
|
|
102
|
+
this.terrainSourceName = 'mapbox-dem';
|
|
103
|
+
this.terrainExaggeration = 1.0;
|
|
104
|
+
this.terrainLayerName = '';
|
|
105
|
+
this.enableSelectingFeatures = this.options.enableSelectingFeatures || false;
|
|
106
|
+
this.enableSelectingObjects = this.options.enableSelectingObjects || false;
|
|
107
|
+
this.enableDraggingObjects = this.options.enableDraggingObjects || false;
|
|
108
|
+
this.enableRotatingObjects = this.options.enableRotatingObjects || false;
|
|
109
|
+
this.enableTooltips = this.options.enableTooltips || false;
|
|
110
|
+
this.multiLayer = this.options.multiLayer || false;
|
|
111
|
+
this.enableHelpTooltips = this.options.enableHelpTooltips || false;
|
|
112
|
+
|
|
113
|
+
this.map.on('style.load', function () {
|
|
114
|
+
this.tb.zoomLayers = [];
|
|
115
|
+
//[jscastro] if multiLayer, create a by default layer in the map, so tb.update won't be needed in client side to avoid duplicating calls to render
|
|
116
|
+
if (this.tb.options.multiLayer) this.addLayer({ id: "threebox_layer", type: 'custom', renderingMode: '3d', map: this, onAdd: function (map, gl) { }, render: function (gl, matrix) { this.map.tb.update(); } })
|
|
117
|
+
|
|
118
|
+
this.once('idle', () => {
|
|
119
|
+
this.tb.setObjectsScale();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (this.tb.options.sky) {
|
|
123
|
+
this.tb.sky = true;
|
|
124
|
+
}
|
|
125
|
+
if (this.tb.options.terrain) {
|
|
126
|
+
this.tb.terrain = true;
|
|
127
|
+
}
|
|
128
|
+
let rasterLayers = ['satellite', 'mapbox-mapbox-satellite', 'satelliteLayer'];
|
|
129
|
+
rasterLayers.forEach((l) => {
|
|
130
|
+
if (this.getLayer(l)) this.tb.terrainLayerName = l;
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
//[jscastro] new event map on load
|
|
135
|
+
this.map.on('load', function () {
|
|
136
|
+
|
|
137
|
+
//[jscastro] new fields to manage events on map
|
|
138
|
+
this.selectedObject; //selected object through click
|
|
139
|
+
this.selectedFeature;//selected state id for extrusion layer features
|
|
140
|
+
this.draggedObject; //dragged object through mousedown + mousemove
|
|
141
|
+
let draggedAction; //dragged action to notify frontend
|
|
142
|
+
this.overedObject; //overed object through mouseover
|
|
143
|
+
this.overedFeature; //overed state for extrusion layer features
|
|
144
|
+
|
|
145
|
+
let canvas = this.getCanvasContainer();
|
|
146
|
+
this.getCanvasContainer().style.cursor = this.tb.defaultCursor;
|
|
147
|
+
// Variable to hold the starting xy coordinates
|
|
148
|
+
// when 'mousedown' occured.
|
|
149
|
+
let start;
|
|
150
|
+
|
|
151
|
+
//when object selected
|
|
152
|
+
let startCoords = [];
|
|
153
|
+
|
|
154
|
+
let lngDiff; // difference between cursor and model left corner
|
|
155
|
+
let latDiff; // difference between cursor and model bottom corner
|
|
156
|
+
let altDiff; // difference between cursor and model height
|
|
157
|
+
let rotationDiff;
|
|
158
|
+
|
|
159
|
+
// Return the xy coordinates of the mouse position
|
|
160
|
+
function mousePos(e) {
|
|
161
|
+
var rect = canvas.getBoundingClientRect();
|
|
162
|
+
return {
|
|
163
|
+
x: e.originalEvent.clientX - rect.left - canvas.clientLeft,
|
|
164
|
+
y: e.originalEvent.clientY - rect.top - canvas.clientTop
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.unselectObject = function () {
|
|
169
|
+
//deselect, reset and return
|
|
170
|
+
this.selectedObject.selected = false;
|
|
171
|
+
this.selectedObject = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.outObject = function () {
|
|
175
|
+
this.overedObject.over = false;
|
|
176
|
+
this.overedObject = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.unselectFeature = function (f) {
|
|
180
|
+
if (typeof f.id == 'undefined') return;
|
|
181
|
+
this.setFeatureState(
|
|
182
|
+
{ source: f.source, sourceLayer: f.sourceLayer, id: f.id },
|
|
183
|
+
{ select: false }
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
this.removeTooltip(f);
|
|
187
|
+
f = this.queryRenderedFeatures({ layers: [f.layer.id], filter: ["==", ['id'], f.id] })[0];
|
|
188
|
+
// Dispatch new event f for unselected
|
|
189
|
+
if (f) this.fire('SelectedFeatureChange', { detail: f });
|
|
190
|
+
this.selectedFeature = null;
|
|
191
|
+
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.selectFeature = function(f) {
|
|
195
|
+
this.selectedFeature = f;
|
|
196
|
+
this.setFeatureState(
|
|
197
|
+
{ source: this.selectedFeature.source, sourceLayer: this.selectedFeature.sourceLayer, id: this.selectedFeature.id },
|
|
198
|
+
{ select: true }
|
|
199
|
+
);
|
|
200
|
+
this.selectedFeature = this.queryRenderedFeatures({ layers: [this.selectedFeature.layer.id], filter: ["==", ['id'], this.selectedFeature.id] })[0];
|
|
201
|
+
this.addTooltip(this.selectedFeature);
|
|
202
|
+
// Dispatch new event SelectedFeature for selected
|
|
203
|
+
this.fire('SelectedFeatureChange', { detail: this.selectedFeature });
|
|
204
|
+
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.outFeature = function(f) {
|
|
208
|
+
if (this.overedFeature && typeof this.overedFeature != 'undefined' && this.overedFeature.id != f) {
|
|
209
|
+
map.setFeatureState(
|
|
210
|
+
{ source: this.overedFeature.source, sourceLayer: this.overedFeature.sourceLayer, id: this.overedFeature.id },
|
|
211
|
+
{ hover: false }
|
|
212
|
+
);
|
|
213
|
+
this.removeTooltip(this.overedFeature);
|
|
214
|
+
this.overedFeature = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.addTooltip = function(f) {
|
|
219
|
+
if (!this.tb.enableTooltips) return;
|
|
220
|
+
let coordinates = this.tb.getFeatureCenter(f);
|
|
221
|
+
let t = this.tb.tooltip({
|
|
222
|
+
text: f.properties.name || f.id || f.type,
|
|
223
|
+
mapboxStyle: true,
|
|
224
|
+
feature: f
|
|
225
|
+
});
|
|
226
|
+
t.setCoords(coordinates);
|
|
227
|
+
this.tb.add(t, f.layer.id);
|
|
228
|
+
f.tooltip = t;
|
|
229
|
+
f.tooltip.tooltip.visible = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.removeTooltip = function(f) {
|
|
233
|
+
if (f.tooltip) {
|
|
234
|
+
f.tooltip.visibility = false;
|
|
235
|
+
this.tb.remove(f.tooltip);
|
|
236
|
+
f.tooltip = null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
map.onContextMenu = function (e) {
|
|
241
|
+
alert('contextMenu'); //TODO: implement a callback
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// onclick function
|
|
245
|
+
this.onClick = function (e) {
|
|
246
|
+
let intersectionExists
|
|
247
|
+
let intersects = [];
|
|
248
|
+
if (map.tb.enableSelectingObjects) {
|
|
249
|
+
//raycast only if we are in a custom layer, for other layers go to the else, this avoids duplicated calls to raycaster
|
|
250
|
+
intersects = this.tb.queryRenderedFeatures(e.point);
|
|
251
|
+
}
|
|
252
|
+
intersectionExists = typeof intersects[0] == 'object';
|
|
253
|
+
// if intersect exists, highlight it
|
|
254
|
+
if (intersectionExists) {
|
|
255
|
+
|
|
256
|
+
let nearestObject = Threebox.prototype.findParent3DObject(intersects[0]);
|
|
257
|
+
|
|
258
|
+
if (nearestObject) {
|
|
259
|
+
//if extrusion object selected, unselect
|
|
260
|
+
if (this.selectedFeature) {
|
|
261
|
+
this.unselectFeature(this.selectedFeature);
|
|
262
|
+
}
|
|
263
|
+
//if not selected yet, select it
|
|
264
|
+
if (!this.selectedObject) {
|
|
265
|
+
this.selectedObject = nearestObject;
|
|
266
|
+
this.selectedObject.selected = true;
|
|
267
|
+
}
|
|
268
|
+
else if (this.selectedObject.uuid != nearestObject.uuid) {
|
|
269
|
+
//it's a different object, restore the previous and select the new one
|
|
270
|
+
this.selectedObject.selected = false;
|
|
271
|
+
nearestObject.selected = true;
|
|
272
|
+
this.selectedObject = nearestObject;
|
|
273
|
+
|
|
274
|
+
} else if (this.selectedObject.uuid == nearestObject.uuid) {
|
|
275
|
+
//deselect, reset and return
|
|
276
|
+
this.unselectObject();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// fire the Wireframed event to notify UI status change
|
|
281
|
+
this.selectedObject.dispatchEvent({ type: 'Wireframed', detail: this.selectedObject });
|
|
282
|
+
this.selectedObject.dispatchEvent({ type: 'IsPlayingChanged', detail: this.selectedObject });
|
|
283
|
+
|
|
284
|
+
this.repaint = true;
|
|
285
|
+
e.preventDefault();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
let features = [];
|
|
290
|
+
if (map.tb.enableSelectingFeatures) {
|
|
291
|
+
features = this.queryRenderedFeatures(e.point);
|
|
292
|
+
}
|
|
293
|
+
//now let's check the extrusion layer objects
|
|
294
|
+
if (features.length > 0) {
|
|
295
|
+
|
|
296
|
+
if (features[0].layer.type == 'fill-extrusion' && typeof features[0].id != 'undefined') {
|
|
297
|
+
|
|
298
|
+
//if 3D object selected, unselect
|
|
299
|
+
if (this.selectedObject) {
|
|
300
|
+
this.unselectObject();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
//if not selected yet, select it
|
|
304
|
+
if (!this.selectedFeature) {
|
|
305
|
+
this.selectFeature(features[0])
|
|
306
|
+
}
|
|
307
|
+
else if (this.selectedFeature.id != features[0].id) {
|
|
308
|
+
//it's a different feature, restore the previous and select the new one
|
|
309
|
+
this.unselectFeature(this.selectedFeature);
|
|
310
|
+
this.selectFeature(features[0])
|
|
311
|
+
|
|
312
|
+
} else if (this.selectedFeature.id == features[0].id) {
|
|
313
|
+
//deselect, reset and return
|
|
314
|
+
this.unselectFeature(this.selectedFeature);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.onMouseMove = function (e) {
|
|
324
|
+
|
|
325
|
+
// Capture the ongoing xy coordinates
|
|
326
|
+
let current = mousePos(e);
|
|
327
|
+
|
|
328
|
+
this.getCanvasContainer().style.cursor = this.tb.defaultCursor;
|
|
329
|
+
//check if being rotated
|
|
330
|
+
if (e.originalEvent.altKey && this.draggedObject) {
|
|
331
|
+
|
|
332
|
+
if (!map.tb.enableRotatingObjects) return;
|
|
333
|
+
draggedAction = 'rotate';
|
|
334
|
+
// Set a UI indicator for dragging.
|
|
335
|
+
this.getCanvasContainer().style.cursor = 'move';
|
|
336
|
+
var minX = Math.min(start.x, current.x),
|
|
337
|
+
maxX = Math.max(start.x, current.x),
|
|
338
|
+
minY = Math.min(start.y, current.y),
|
|
339
|
+
maxY = Math.max(start.y, current.y);
|
|
340
|
+
//set the movement fluid we rotate only every 10px moved, in steps of 10 degrees up to 360
|
|
341
|
+
let rotation = { x: 0, y: 0, z: (Math.round(rotationDiff[2] + (~~((current.x - start.x) / this.tb.rotationStep) % 360 * this.tb.rotationStep) % 360)) };
|
|
342
|
+
//now rotate the model depending the axis
|
|
343
|
+
this.draggedObject.setRotation(rotation);
|
|
344
|
+
if (map.tb.enableHelpTooltips) this.draggedObject.addHelp("rot: " + rotation.z + "°");
|
|
345
|
+
//this.draggedObject.setRotationAxis(rotation);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
//check if being moved
|
|
350
|
+
if (e.originalEvent.shiftKey && this.draggedObject) {
|
|
351
|
+
if (!map.tb.enableDraggingObjects) return;
|
|
352
|
+
|
|
353
|
+
draggedAction = 'translate';
|
|
354
|
+
// Set a UI indicator for dragging.
|
|
355
|
+
this.getCanvasContainer().style.cursor = 'move';
|
|
356
|
+
// Capture the first xy coordinates, height must be the same to move on the same plane
|
|
357
|
+
let coords = e.lngLat;
|
|
358
|
+
let options = [Number((coords.lng + lngDiff).toFixed(this.tb.gridStep)), Number((coords.lat + latDiff).toFixed(this.tb.gridStep)), this.draggedObject.modelHeight];
|
|
359
|
+
this.draggedObject.setCoords(options);
|
|
360
|
+
if (map.tb.enableHelpTooltips) this.draggedObject.addHelp("lng: " + options[0] + "°, lat: " + options[1] + "°");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
//check if being moved on altitude
|
|
365
|
+
if (e.originalEvent.ctrlKey && this.draggedObject) {
|
|
366
|
+
if (!map.tb.enableDraggingObjects) return;
|
|
367
|
+
draggedAction = 'altitude';
|
|
368
|
+
// Set a UI indicator for dragging.
|
|
369
|
+
this.getCanvasContainer().style.cursor = 'move';
|
|
370
|
+
// Capture the first xy coordinates, height must be the same to move on the same plane
|
|
371
|
+
let now = (e.point.y * this.tb.altitudeStep);
|
|
372
|
+
let options = [this.draggedObject.coordinates[0], this.draggedObject.coordinates[1], Number((- now - altDiff).toFixed(this.tb.gridStep))];
|
|
373
|
+
this.draggedObject.setCoords(options);
|
|
374
|
+
if (map.tb.enableHelpTooltips) this.draggedObject.addHelp("alt: " + options[2] + "m");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let intersectionExists
|
|
379
|
+
let intersects = [];
|
|
380
|
+
|
|
381
|
+
if (map.tb.enableSelectingObjects) {
|
|
382
|
+
// calculate objects intersecting the picking ray
|
|
383
|
+
intersects = this.tb.queryRenderedFeatures(e.point);
|
|
384
|
+
}
|
|
385
|
+
intersectionExists = typeof intersects[0] == 'object';
|
|
386
|
+
|
|
387
|
+
// if intersect exists, highlight it, if not check the extrusion layer
|
|
388
|
+
if (intersectionExists) {
|
|
389
|
+
let nearestObject = Threebox.prototype.findParent3DObject(intersects[0]);
|
|
390
|
+
if (nearestObject) {
|
|
391
|
+
this.outFeature(this.overedFeature);
|
|
392
|
+
this.getCanvasContainer().style.cursor = 'pointer';
|
|
393
|
+
if (!this.selectedObject || nearestObject.uuid != this.selectedObject.uuid) {
|
|
394
|
+
if (this.overedObject && this.overedObject.uuid != nearestObject.uuid) {
|
|
395
|
+
this.outObject();
|
|
396
|
+
}
|
|
397
|
+
nearestObject.over = true;
|
|
398
|
+
this.overedObject = nearestObject;
|
|
399
|
+
} else if (this.selectedObject && nearestObject.uuid == this.selectedObject.uuid) {
|
|
400
|
+
nearestObject.over = true;
|
|
401
|
+
this.overedObject = nearestObject;
|
|
402
|
+
}
|
|
403
|
+
this.repaint = true;
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
//clean the object overed
|
|
409
|
+
if (this.overedObject) { this.outObject(); }
|
|
410
|
+
//now let's check the extrusion layer objects
|
|
411
|
+
let features = [];
|
|
412
|
+
if (map.tb.enableSelectingFeatures) {
|
|
413
|
+
features = this.queryRenderedFeatures(e.point);
|
|
414
|
+
}
|
|
415
|
+
if (features.length > 0) {
|
|
416
|
+
this.outFeature(features[0]);
|
|
417
|
+
|
|
418
|
+
if (features[0].layer.type == 'fill-extrusion' && typeof features[0].id != 'undefined') {
|
|
419
|
+
if ((!this.selectedFeature || this.selectedFeature.id != features[0].id)) {
|
|
420
|
+
this.getCanvasContainer().style.cursor = 'pointer';
|
|
421
|
+
this.overedFeature = features[0];
|
|
422
|
+
this.setFeatureState(
|
|
423
|
+
{ source: this.overedFeature.source, sourceLayer: this.overedFeature.sourceLayer, id: this.overedFeature.id },
|
|
424
|
+
{ hover: true }
|
|
425
|
+
);
|
|
426
|
+
this.overedFeature = map.queryRenderedFeatures({ layers: [this.overedFeature.layer.id], filter: ["==", ['id'], this.overedFeature.id] })[0];
|
|
427
|
+
this.addTooltip(this.overedFeature);
|
|
428
|
+
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
this.onMouseDown = function (e) {
|
|
437
|
+
|
|
438
|
+
// Continue the rest of the function shiftkey or altkey are pressed, and if object is selected
|
|
439
|
+
if (!((e.originalEvent.shiftKey || e.originalEvent.altKey || e.originalEvent.ctrlKey) && e.originalEvent.button === 0 && this.selectedObject)) return;
|
|
440
|
+
if (!map.tb.enableDraggingObjects && !map.tb.enableRotatingObjects) return;
|
|
441
|
+
|
|
442
|
+
e.preventDefault();
|
|
443
|
+
|
|
444
|
+
map.getCanvasContainer().style.cursor = 'move';
|
|
445
|
+
|
|
446
|
+
// Disable default drag zooming when the shift key is held down.
|
|
447
|
+
//map.dragPan.disable();
|
|
448
|
+
|
|
449
|
+
// Call functions for the following events
|
|
450
|
+
map.once('mouseup', this.onMouseUp);
|
|
451
|
+
//map.once('mouseout', this.onMouseUp);
|
|
452
|
+
|
|
453
|
+
// move the selected object
|
|
454
|
+
this.draggedObject = this.selectedObject;
|
|
455
|
+
|
|
456
|
+
// Capture the first xy coordinates
|
|
457
|
+
start = mousePos(e);
|
|
458
|
+
startCoords = this.draggedObject.coordinates;
|
|
459
|
+
|
|
460
|
+
rotationDiff = utils.degreeify(this.draggedObject.rotation);
|
|
461
|
+
lngDiff = startCoords[0] - e.lngLat.lng;
|
|
462
|
+
latDiff = startCoords[1] - e.lngLat.lat;
|
|
463
|
+
altDiff = -this.draggedObject.modelHeight - (e.point.y * this.tb.altitudeStep);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.onMouseUp = function (e) {
|
|
467
|
+
|
|
468
|
+
// Set a UI indicator for dragging.
|
|
469
|
+
this.getCanvasContainer().style.cursor = this.tb.defaultCursor;
|
|
470
|
+
|
|
471
|
+
// Remove these events now that finish has been called.
|
|
472
|
+
//map.off('mousemove', onMouseMove);
|
|
473
|
+
this.off('mouseup', this.onMouseUp);
|
|
474
|
+
this.off('mouseout', this.onMouseUp);
|
|
475
|
+
this.dragPan.enable();
|
|
476
|
+
|
|
477
|
+
if (this.draggedObject) {
|
|
478
|
+
this.draggedObject.dispatchEvent({ type: 'ObjectDragged', detail: { draggedObject: this.draggedObject, draggedAction: draggedAction } });
|
|
479
|
+
this.draggedObject.removeHelp();
|
|
480
|
+
this.draggedObject = null;
|
|
481
|
+
draggedAction = null;
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this.onMouseOut = function (e) {
|
|
486
|
+
if (this.overedFeature) {
|
|
487
|
+
let features = this.queryRenderedFeatures(e.point);
|
|
488
|
+
if (features.length > 0 && this.overedFeature.id != features[0].id) {
|
|
489
|
+
this.getCanvasContainer().style.cursor = this.tb.defaultCursor;
|
|
490
|
+
//only unover when new feature is another
|
|
491
|
+
this.outFeature(features[0]);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
this.onZoom = function (e) {
|
|
497
|
+
this.tb.zoomLayers.forEach((l) => { this.tb.toggleLayer(l); });
|
|
498
|
+
this.tb.setObjectsScale();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let ctrlDown = false;
|
|
502
|
+
let shiftDown = false;
|
|
503
|
+
let ctrlKey = 17, cmdKey = 91, shiftKey = 16, sK = 83, dK = 68;
|
|
504
|
+
|
|
505
|
+
function onKeyDown(e) {
|
|
506
|
+
|
|
507
|
+
if (e.which === ctrlKey || e.which === cmdKey) ctrlDown = true;
|
|
508
|
+
if (e.which === shiftKey) shiftDown = true;
|
|
509
|
+
let obj = this.selectedObject;
|
|
510
|
+
if (shiftDown && e.which === sK && obj) {
|
|
511
|
+
//shift + sS
|
|
512
|
+
let dc = utils.toDecimal;
|
|
513
|
+
if (!obj.help) {
|
|
514
|
+
let s = obj.modelSize;
|
|
515
|
+
let sf = 1;
|
|
516
|
+
if (obj.userData.units !== 'meters') {
|
|
517
|
+
//if not meters, calculate scale to the current lat
|
|
518
|
+
sf = utils.projectedUnitsPerMeter(obj.coordinates[1]);
|
|
519
|
+
if (!sf) { sf = 1; };
|
|
520
|
+
sf = dc(sf, 7);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (map.tb.enableHelpTooltips) obj.addHelp("size(m): " + dc((s.x / sf), 3) + " W, " + dc((s.y / sf), 3) + " L, " + dc((s.z / sf), 3) + " H");
|
|
524
|
+
this.repaint = true;
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
obj.removeHelp();
|
|
528
|
+
}
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
function onKeyUp (e) {
|
|
535
|
+
if (e.which == ctrlKey || e.which == cmdKey) ctrlDown = false;
|
|
536
|
+
if (e.which === shiftKey) shiftDown = false;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
//listener to the events
|
|
540
|
+
//this.on('contextmenu', map.onContextMenu);
|
|
541
|
+
this.on('click', this.onClick);
|
|
542
|
+
this.on('mousemove', this.onMouseMove);
|
|
543
|
+
this.on('mouseout', this.onMouseOut)
|
|
544
|
+
this.on('mousedown', this.onMouseDown);
|
|
545
|
+
this.on('zoom', this.onZoom);
|
|
546
|
+
this.on('zoomend', this.onZoom);
|
|
547
|
+
|
|
548
|
+
document.addEventListener('keydown', onKeyDown.bind(this), true);
|
|
549
|
+
document.addEventListener('keyup', onKeyUp.bind(this));
|
|
550
|
+
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
//[jscastro] added property to manage an athmospheric sky layer
|
|
556
|
+
get sky() { return this.options.sky; },
|
|
557
|
+
set sky(value) {
|
|
558
|
+
if (value) {
|
|
559
|
+
this.createSkyLayer();
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
this.removeLayer(this.skyLayerName);
|
|
563
|
+
}
|
|
564
|
+
this.options.sky = value;
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
//[jscastro] added property to manage an athmospheric sky layer
|
|
568
|
+
get terrain() { return this.options.terrain; },
|
|
569
|
+
set terrain(value) {
|
|
570
|
+
this.terrainLayerName = '';
|
|
571
|
+
if (value) {
|
|
572
|
+
this.createTerrainLayer();
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
if (this.mapboxVersion < 2.0) { console.warn("Terrain layer are only supported by Mapbox-gl-js > v2.0"); return };
|
|
576
|
+
|
|
577
|
+
if (this.map.getTerrain()) {
|
|
578
|
+
this.map.setTerrain(null); //
|
|
579
|
+
this.map.removeSource(this.terrainSourceName);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
this.options.terrain = value;
|
|
583
|
+
},
|
|
584
|
+
|
|
585
|
+
//[jscastro] added property to manage FOV for perspective camera
|
|
586
|
+
get fov() { return this.options.fov;},
|
|
587
|
+
set fov(value) {
|
|
588
|
+
if (this.camera instanceof THREE.PerspectiveCamera && this.options.fov !== value) {
|
|
589
|
+
this.map.transform.fov = value;
|
|
590
|
+
this.camera.fov = this.map.transform.fov;
|
|
591
|
+
this.cameraSync.setupCamera();
|
|
592
|
+
this.map.repaint = true;
|
|
593
|
+
this.options.fov = value;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
//[jscastro] added property to manage camera type
|
|
599
|
+
get orthographic() { return this.options.orthographic; },
|
|
600
|
+
set orthographic(value) {
|
|
601
|
+
const h = this.map.getCanvas().clientHeight;
|
|
602
|
+
const w = this.map.getCanvas().clientWidth;
|
|
603
|
+
if (value) {
|
|
604
|
+
this.map.transform.fov = 0;
|
|
605
|
+
this.camera = new THREE.OrthographicCamera(w / - 2, w / 2, h / 2, h / - 2, 0.1, 1e21);
|
|
606
|
+
} else {
|
|
607
|
+
this.map.transform.fov = this.fov;
|
|
608
|
+
this.camera = new THREE.PerspectiveCamera(this.map.transform.fov, w / h, 0.1, 1e21);
|
|
609
|
+
}
|
|
610
|
+
this.camera.layers.enable(0);
|
|
611
|
+
this.camera.layers.enable(1);
|
|
612
|
+
// The CameraSync object will keep the Mapbox and THREE.js camera movements in sync.
|
|
613
|
+
// It requires a world group to scale as we zoom in. Rotation is handled in the camera's
|
|
614
|
+
// projection matrix itself (as is field of view and near/far clipping)
|
|
615
|
+
// It automatically registers to listen for move events on the map so we don't need to do that here
|
|
616
|
+
this.cameraSync = new CameraSync(this.map, this.camera, this.world);
|
|
617
|
+
this.map.repaint = true; // repaint the map
|
|
618
|
+
this.options.orthographic = value;
|
|
619
|
+
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
//[jscastro] method to create an athmospheric sky layer
|
|
623
|
+
createSkyLayer: function () {
|
|
624
|
+
if (this.mapboxVersion < 2.0) { console.warn("Sky layer are only supported by Mapbox-gl-js > v2.0"); this.options.sky = false; return };
|
|
625
|
+
|
|
626
|
+
let layer = this.map.getLayer(this.skyLayerName);
|
|
627
|
+
if (!layer) {
|
|
628
|
+
this.map.addLayer({
|
|
629
|
+
'id': this.skyLayerName,
|
|
630
|
+
'type': 'sky',
|
|
631
|
+
'paint': {
|
|
632
|
+
'sky-opacity': [
|
|
633
|
+
'interpolate',
|
|
634
|
+
['linear'],
|
|
635
|
+
['zoom'],
|
|
636
|
+
0,
|
|
637
|
+
0,
|
|
638
|
+
5,
|
|
639
|
+
0.3,
|
|
640
|
+
8,
|
|
641
|
+
1
|
|
642
|
+
],
|
|
643
|
+
// set up the sky layer for atmospheric scattering
|
|
644
|
+
'sky-type': 'atmosphere',
|
|
645
|
+
// explicitly set the position of the sun rather than allowing the sun to be attached to the main light source
|
|
646
|
+
'sky-atmosphere-sun': this.getSunSky(this.lightDateTime),
|
|
647
|
+
// set the intensity of the sun as a light source (0-100 with higher values corresponding to brighter skies)
|
|
648
|
+
'sky-atmosphere-sun-intensity': 10
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
this.map.once('idle', () => {
|
|
653
|
+
this.setSunlight();
|
|
654
|
+
this.repaint();
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
//[jscastro] method to create a terrain layer
|
|
660
|
+
createTerrainLayer: function () {
|
|
661
|
+
if (this.mapboxVersion < 2.0) { console.warn("Terrain layer are only supported by Mapbox-gl-js > v2.0"); this.options.terrain = false; return };
|
|
662
|
+
let layer = this.map.getTerrain();
|
|
663
|
+
if (!layer) {
|
|
664
|
+
// add the DEM source as a terrain layer with exaggerated height
|
|
665
|
+
this.map.addSource(this.terrainSourceName, {
|
|
666
|
+
'type': 'raster-dem',
|
|
667
|
+
'url': 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
|
668
|
+
'tileSize': 512,
|
|
669
|
+
'maxzoom': 14
|
|
670
|
+
});
|
|
671
|
+
this.map.setTerrain({ 'source': this.terrainSourceName, 'exaggeration': this.terrainExaggeration });
|
|
672
|
+
this.map.once('idle', () => {
|
|
673
|
+
//alert("idle");
|
|
674
|
+
this.cameraSync.updateCamera();
|
|
675
|
+
this.repaint();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
|
|
681
|
+
// Objects
|
|
682
|
+
sphere: function (options) {
|
|
683
|
+
this.setDefaultView(options, this.options);
|
|
684
|
+
return sphere(options, this.world)
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
line: line,
|
|
688
|
+
|
|
689
|
+
label: label,
|
|
690
|
+
|
|
691
|
+
tooltip: tooltip,
|
|
692
|
+
|
|
693
|
+
tube: function (options) {
|
|
694
|
+
this.setDefaultView(options, this.options);
|
|
695
|
+
return tube(options, this.world)
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
extrusion: function (options) {
|
|
699
|
+
this.setDefaultView(options, this.options);
|
|
700
|
+
return extrusion(options);
|
|
701
|
+
},
|
|
702
|
+
|
|
703
|
+
Object3D: function (options) {
|
|
704
|
+
this.setDefaultView(options, this.options);
|
|
705
|
+
return Object3D(options)
|
|
706
|
+
},
|
|
707
|
+
|
|
708
|
+
loadObj: async function loadObj(options, cb) {
|
|
709
|
+
this.setDefaultView(options, this.options);
|
|
710
|
+
if (options.clone === false) {
|
|
711
|
+
return new Promise(
|
|
712
|
+
async (resolve) => {
|
|
713
|
+
loader(options, cb, async (obj) => {
|
|
714
|
+
resolve(obj);
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
//[jscastro] new added cache for 3D Objects
|
|
720
|
+
let cache = this.objectsCache.get(options.obj);
|
|
721
|
+
if (cache) {
|
|
722
|
+
cache.promise
|
|
723
|
+
.then(obj => {
|
|
724
|
+
cb(obj.duplicate(options));
|
|
725
|
+
})
|
|
726
|
+
.catch(err => {
|
|
727
|
+
this.objectsCache.delete(options.obj);
|
|
728
|
+
console.error("Could not load model file: " + options.obj);
|
|
729
|
+
});
|
|
730
|
+
} else {
|
|
731
|
+
this.objectsCache.set(options.obj, {
|
|
732
|
+
promise: new Promise(
|
|
733
|
+
async (resolve, reject) => {
|
|
734
|
+
loader(options, cb, async (obj) => {
|
|
735
|
+
if (obj.duplicate) {
|
|
736
|
+
resolve(obj.duplicate());
|
|
737
|
+
} else {
|
|
738
|
+
reject(obj);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
})
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
|
|
748
|
+
// Material
|
|
749
|
+
|
|
750
|
+
material: function (o) {
|
|
751
|
+
return material(o)
|
|
752
|
+
},
|
|
753
|
+
|
|
754
|
+
initLights : {
|
|
755
|
+
ambientLight: null,
|
|
756
|
+
dirLight: null,
|
|
757
|
+
dirLightBack: null,
|
|
758
|
+
dirLightHelper: null,
|
|
759
|
+
hemiLight: null,
|
|
760
|
+
pointLight: null
|
|
761
|
+
},
|
|
762
|
+
|
|
763
|
+
utils: utils,
|
|
764
|
+
|
|
765
|
+
SunCalc: SunCalc,
|
|
766
|
+
|
|
767
|
+
Constants: ThreeboxConstants,
|
|
768
|
+
|
|
769
|
+
projectToWorld: function (coords) {
|
|
770
|
+
return this.utils.projectToWorld(coords)
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
unprojectFromWorld: function (v3) {
|
|
774
|
+
return this.utils.unprojectFromWorld(v3)
|
|
775
|
+
},
|
|
776
|
+
|
|
777
|
+
projectedUnitsPerMeter: function (lat) {
|
|
778
|
+
return this.utils.projectedUnitsPerMeter(lat)
|
|
779
|
+
},
|
|
780
|
+
|
|
781
|
+
//get the center point of a feature
|
|
782
|
+
getFeatureCenter: function getFeatureCenter(feature, obj, level) {
|
|
783
|
+
return utils.getFeatureCenter(feature, obj, level);
|
|
784
|
+
},
|
|
785
|
+
|
|
786
|
+
getObjectHeightOnFloor: function (feature, obj, level) {
|
|
787
|
+
return utils.getObjectHeightOnFloor(feature, obj, level);
|
|
788
|
+
},
|
|
789
|
+
|
|
790
|
+
queryRenderedFeatures: function (point) {
|
|
791
|
+
|
|
792
|
+
let mouse = new THREE.Vector2();
|
|
793
|
+
|
|
794
|
+
// // scale mouse pixel position to a percentage of the screen's width and height
|
|
795
|
+
mouse.x = (point.x / this.map.transform.width) * 2 - 1;
|
|
796
|
+
mouse.y = 1 - (point.y / this.map.transform.height) * 2;
|
|
797
|
+
|
|
798
|
+
this.raycaster.setFromCamera(mouse, this.camera);
|
|
799
|
+
|
|
800
|
+
// calculate objects intersecting the picking ray
|
|
801
|
+
let intersects = this.raycaster.intersectObjects(this.world.children, true);
|
|
802
|
+
|
|
803
|
+
return intersects
|
|
804
|
+
},
|
|
805
|
+
|
|
806
|
+
//[jscastro] find 3D object of a mesh. this method is needed to know the object of a raycasted mesh
|
|
807
|
+
findParent3DObject: function (mesh) {
|
|
808
|
+
//find the Parent Object3D of the mesh captured by Raytracer
|
|
809
|
+
var result;
|
|
810
|
+
mesh.object.traverseAncestors(function (m) {
|
|
811
|
+
if (m.parent)
|
|
812
|
+
if (m.parent.type == "Group" && m.userData.obj) {
|
|
813
|
+
result = m;
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
return result;
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
//[jscastro] method to replicate behaviour of map.setLayoutProperty when Threebox are affected
|
|
820
|
+
setLayoutProperty: function (layerId, name, value) {
|
|
821
|
+
//first set layout property at the map
|
|
822
|
+
this.map.setLayoutProperty(layerId, name, value);
|
|
823
|
+
if (value !== null && value !== undefined) {
|
|
824
|
+
if (name === 'visibility') {
|
|
825
|
+
this.world.children.filter(o => (o.layer === layerId)).forEach((o) => { o.visibility = value });
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
|
|
830
|
+
//[jscastro] Custom Layers doesn't work on minzoom and maxzoom attributes, and if the layer is including labels they don't hide either on minzoom
|
|
831
|
+
setLayerZoomRange: function (layerId, minZoomLayer, maxZoomLayer) {
|
|
832
|
+
if (this.map.getLayer(layerId)) {
|
|
833
|
+
this.map.setLayerZoomRange(layerId, minZoomLayer, maxZoomLayer);
|
|
834
|
+
if (!this.zoomLayers.includes(layerId)) this.zoomLayers.push(layerId);
|
|
835
|
+
this.toggleLayer(layerId);
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
|
|
839
|
+
//[jscastro] method to set the height of all the objects in a level. this only works if the objects have a geojson feature
|
|
840
|
+
setLayerHeigthProperty: function (layerId, level) {
|
|
841
|
+
let layer = this.map.getLayer(layerId);
|
|
842
|
+
if (!layer) return;
|
|
843
|
+
if (layer.type == "fill-extrusion") {
|
|
844
|
+
let data = this.map.getStyle().sources[layer.source].data;
|
|
845
|
+
let features = data.features;
|
|
846
|
+
features.forEach(function (f) {
|
|
847
|
+
f.properties.level = level;
|
|
848
|
+
});
|
|
849
|
+
//we change the level on the source
|
|
850
|
+
this.map.getSource(layer.source).setData(data);
|
|
851
|
+
} else if (layer.type == "custom") {
|
|
852
|
+
this.world.children.forEach(function (obj) {
|
|
853
|
+
let feature = obj.userData.feature;
|
|
854
|
+
if (feature && feature.layer === layerId) {
|
|
855
|
+
//TODO: this could be a multidimensional array
|
|
856
|
+
let location = this.tb.getFeatureCenter(feature, obj, level);
|
|
857
|
+
obj.setCoords(location);
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
//[jscastro] method to set globally all the objects that are fixedScale
|
|
864
|
+
setObjectsScale: function () {
|
|
865
|
+
this.world.children.filter(o => (o.fixedZoom != null)).forEach((o) => { o.setObjectScale(this.map.transform.scale); });
|
|
866
|
+
},
|
|
867
|
+
|
|
868
|
+
//[jscastro] mapbox setStyle removes all the layers, including custom layers, so tb.world must be cleaned up too
|
|
869
|
+
setStyle: function (styleId, options) {
|
|
870
|
+
this.clear().then(() => {
|
|
871
|
+
this.map.setStyle(styleId, options);
|
|
872
|
+
});
|
|
873
|
+
},
|
|
874
|
+
|
|
875
|
+
//[jscastro] method to toggle Layer visibility checking zoom range
|
|
876
|
+
toggleLayer: function (layerId, visible = true) {
|
|
877
|
+
let l = this.map.getLayer(layerId);
|
|
878
|
+
if (l) {
|
|
879
|
+
if (!visible) {
|
|
880
|
+
this.toggle(l.id, false);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
let z = this.map.getZoom();
|
|
884
|
+
if (l.minzoom && z < l.minzoom) { this.toggle(l.id, false); return; };
|
|
885
|
+
if (l.maxzoom && z >= l.maxzoom) { this.toggle(l.id, false); return; };
|
|
886
|
+
this.toggle(l.id, true);
|
|
887
|
+
};
|
|
888
|
+
},
|
|
889
|
+
|
|
890
|
+
//[jscastro] method to toggle Layer visibility
|
|
891
|
+
toggle: function (layerId, visible) {
|
|
892
|
+
//call
|
|
893
|
+
this.setLayoutProperty(layerId, 'visibility', (visible ? 'visible' : 'none'))
|
|
894
|
+
this.labelRenderer.toggleLabels(layerId, visible);
|
|
895
|
+
},
|
|
896
|
+
|
|
897
|
+
update: function () {
|
|
898
|
+
|
|
899
|
+
if (this.map.repaint) this.map.repaint = false
|
|
900
|
+
|
|
901
|
+
var timestamp = Date.now();
|
|
902
|
+
|
|
903
|
+
// Update any animations
|
|
904
|
+
this.objects.animationManager.update(timestamp);
|
|
905
|
+
|
|
906
|
+
this.updateLightHelper();
|
|
907
|
+
|
|
908
|
+
// Render the scene and repaint the map
|
|
909
|
+
this.renderer.resetState(); //update threejs r126
|
|
910
|
+
this.renderer.render(this.scene, this.camera);
|
|
911
|
+
|
|
912
|
+
// [jscastro] Render any label
|
|
913
|
+
this.labelRenderer.render(this.scene, this.camera);
|
|
914
|
+
if (this.options.passiveRendering === false) this.map.triggerRepaint();
|
|
915
|
+
},
|
|
916
|
+
|
|
917
|
+
add: function (obj, layerId, sourceId) {
|
|
918
|
+
//[jscastro] remove the tooltip if not enabled
|
|
919
|
+
if (!this.enableTooltips && obj.tooltip) { obj.tooltip.visibility = false };
|
|
920
|
+
this.world.add(obj);
|
|
921
|
+
if (layerId) {
|
|
922
|
+
obj.layer = layerId;
|
|
923
|
+
obj.source = sourceId;
|
|
924
|
+
let l = this.map.getLayer(layerId);
|
|
925
|
+
if (l) {
|
|
926
|
+
let v = l.visibility;
|
|
927
|
+
let u = typeof v === 'undefined';
|
|
928
|
+
obj.visibility = (u || v === 'visible' ? true : false);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
|
|
933
|
+
removeByName: function (name) {
|
|
934
|
+
let obj = this.world.getObjectByName(name);
|
|
935
|
+
if (obj) this.remove(obj);
|
|
936
|
+
},
|
|
937
|
+
|
|
938
|
+
remove: function (obj) {
|
|
939
|
+
if (this.map.selectedObject && obj.uuid == this.map.selectedObject.uuid) this.map.unselectObject();
|
|
940
|
+
if (this.map.draggedObject && obj.uuid == this.map.draggedObject.uuid) this.map.draggedObject = null;
|
|
941
|
+
if (obj.dispose) obj.dispose();
|
|
942
|
+
this.world.remove(obj);
|
|
943
|
+
obj = null;
|
|
944
|
+
},
|
|
945
|
+
|
|
946
|
+
//[jscastro] this clears tb.world in order to dispose properly the resources
|
|
947
|
+
clear: async function (layerId = null, dispose = false) {
|
|
948
|
+
return new Promise((resolve, reject) => {
|
|
949
|
+
let objects = [];
|
|
950
|
+
this.world.children.forEach(function (object) {
|
|
951
|
+
objects.push(object);
|
|
952
|
+
});
|
|
953
|
+
for (let i = 0; i < objects.length; i++) {
|
|
954
|
+
let obj = objects[i];
|
|
955
|
+
//if layerId, check the layer to remove, otherwise always remove
|
|
956
|
+
if (obj.layer === layerId || !layerId) {
|
|
957
|
+
this.remove(obj);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (dispose) {
|
|
961
|
+
this.objectsCache.forEach((value) => {
|
|
962
|
+
value.promise.then(obj => {
|
|
963
|
+
obj.dispose();
|
|
964
|
+
obj = null;
|
|
965
|
+
})
|
|
966
|
+
})
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
resolve("clear");
|
|
970
|
+
});
|
|
971
|
+
},
|
|
972
|
+
|
|
973
|
+
//[jscastro] remove a layer clearing first the 3D objects from this layer in tb.world
|
|
974
|
+
removeLayer: function (layerId) {
|
|
975
|
+
this.clear(layerId, true).then( () => {
|
|
976
|
+
this.map.removeLayer(layerId);
|
|
977
|
+
});
|
|
978
|
+
},
|
|
979
|
+
|
|
980
|
+
//[jscastro] get the sun position (azimuth, altitude) from a given datetime, lng, lat
|
|
981
|
+
getSunPosition: function (date, coords) {
|
|
982
|
+
return SunCalc.getPosition(date || Date.now(), coords[1], coords[0]);
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
//[jscastro] get the sun times for sunrise, sunset, etc.. from a given datetime, lng, lat and alt
|
|
986
|
+
getSunTimes: function (date, coords) {
|
|
987
|
+
return SunCalc.getTimes(date, coords[1], coords[0], (coords[2] ? coords[2] : 0));
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
//[jscastro] set shadows for fill-extrusion layers
|
|
991
|
+
setBuildingShadows: function (options) {
|
|
992
|
+
if (this.map.getLayer(options.buildingsLayerId)) {
|
|
993
|
+
let layer = new BuildingShadows(options, this);
|
|
994
|
+
this.map.addLayer(layer, options.buildingsLayerId);
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
console.warn("The layer '" + options.buildingsLayerId + "' does not exist in the map.");
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
|
|
1001
|
+
//[jscastro] This method set the sun light for a given datetime and lnglat
|
|
1002
|
+
setSunlight: function (newDate = new Date(), coords) {
|
|
1003
|
+
if (!this.lights.dirLight || !this.options.realSunlight) {
|
|
1004
|
+
console.warn("To use setSunlight it's required to set realSunlight : true in Threebox initial options.");
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
var date = new Date(newDate.getTime());
|
|
1009
|
+
|
|
1010
|
+
if (coords) {
|
|
1011
|
+
if (coords.lng && coords.lat) this.mapCenter = coords
|
|
1012
|
+
else this.mapCenter = { lng: coords[0], lat: coords[1] };
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
this.mapCenter = this.map.getCenter();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (this.lightDateTime && this.lightDateTime.getTime() === date.getTime() && this.lightLng === this.mapCenter.lng && this.lightLat === this.mapCenter.lat) {
|
|
1019
|
+
return; //setSunLight could be called on render, so due to performance, avoid duplicated calls
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
this.lightDateTime = date;
|
|
1023
|
+
this.lightLng = this.mapCenter.lng;
|
|
1024
|
+
this.lightLat = this.mapCenter.lat
|
|
1025
|
+
this.sunPosition = this.getSunPosition(date, [this.mapCenter.lng, this.mapCenter.lat]);
|
|
1026
|
+
let altitude = this.sunPosition.altitude;
|
|
1027
|
+
let azimuth = Math.PI + this.sunPosition.azimuth;
|
|
1028
|
+
//console.log("Altitude: " + utils.degreeify(altitude) + ", Azimuth: " + (utils.degreeify(azimuth)));
|
|
1029
|
+
|
|
1030
|
+
let radius = ThreeboxConstants.WORLD_SIZE / 2;
|
|
1031
|
+
let alt = Math.sin(altitude);
|
|
1032
|
+
let altRadius = Math.cos(altitude);
|
|
1033
|
+
let azCos = Math.cos(azimuth) * altRadius;
|
|
1034
|
+
let azSin = Math.sin(azimuth) * altRadius;
|
|
1035
|
+
|
|
1036
|
+
this.lights.dirLight.position.set(azSin, azCos, alt);
|
|
1037
|
+
this.lights.dirLight.position.multiplyScalar(radius);
|
|
1038
|
+
this.lights.dirLight.intensity = Math.max(alt, 0);
|
|
1039
|
+
this.lights.hemiLight.intensity = Math.max(alt * 1, 0.1);
|
|
1040
|
+
//console.log("Intensity:" + this.lights.dirLight.intensity);
|
|
1041
|
+
this.lights.dirLight.updateMatrixWorld();
|
|
1042
|
+
this.updateLightHelper();
|
|
1043
|
+
if (this.map.loaded()) {
|
|
1044
|
+
this.updateSunGround(this.sunPosition);
|
|
1045
|
+
this.map.setLight({
|
|
1046
|
+
anchor: 'map',
|
|
1047
|
+
position: [3, 180 + this.sunPosition.azimuth * 180 / Math.PI, 90 - this.sunPosition.altitude * 180 / Math.PI],
|
|
1048
|
+
intensity: Math.cos(this.sunPosition.altitude), //0.4,
|
|
1049
|
+
color: `hsl(40, ${50 * Math.cos(this.sunPosition.altitude)}%, ${Math.max(20, 20 + (96 * Math.sin(this.sunPosition.altitude)))}%)`
|
|
1050
|
+
|
|
1051
|
+
}, { duration: 0 });
|
|
1052
|
+
if (this.sky) { this.updateSunSky(this.getSunSky(date, this.sunPosition));}
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
|
|
1056
|
+
getSunSky: function (date, sunPos) {
|
|
1057
|
+
if (!sunPos) {
|
|
1058
|
+
var center = this.map.getCenter();
|
|
1059
|
+
sunPos = this.getSunPosition(
|
|
1060
|
+
date || Date.now(), [center.lng, center.lat]
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
var sunAzimuth = 180 + (sunPos.azimuth * 180) / Math.PI;
|
|
1064
|
+
var sunAltitude = 90 - (sunPos.altitude * 180) / Math.PI;
|
|
1065
|
+
return [sunAzimuth, sunAltitude];
|
|
1066
|
+
},
|
|
1067
|
+
|
|
1068
|
+
updateSunSky: function (sunPos) {
|
|
1069
|
+
if (this.sky) {
|
|
1070
|
+
// update the `sky-atmosphere-sun` paint property with the position of the sun based on the selected time
|
|
1071
|
+
this.map.setPaintProperty(this.skyLayerName, 'sky-atmosphere-sun', sunPos);
|
|
1072
|
+
}
|
|
1073
|
+
},
|
|
1074
|
+
|
|
1075
|
+
updateSunGround: function (sunPos) {
|
|
1076
|
+
if (this.terrainLayerName != '') {
|
|
1077
|
+
// update the raster layer paint property with the position of the sun based on the selected time
|
|
1078
|
+
this.map.setPaintProperty(this.terrainLayerName, 'raster-opacity', Math.max(Math.min(1, sunPos.altitude * 4), 0.25));
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
|
|
1082
|
+
//[jscastro] this updates the directional light helper
|
|
1083
|
+
updateLightHelper: function () {
|
|
1084
|
+
if (this.lights.dirLightHelper) {
|
|
1085
|
+
this.lights.dirLightHelper.position.setFromMatrixPosition(this.lights.dirLight.matrixWorld);
|
|
1086
|
+
this.lights.dirLightHelper.updateMatrix();
|
|
1087
|
+
this.lights.dirLightHelper.update();
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
|
|
1091
|
+
//[jscastro] method to fully dispose the resources, watch out is you call this without navigating to other page
|
|
1092
|
+
dispose: async function () {
|
|
1093
|
+
|
|
1094
|
+
console.log(this.memory());
|
|
1095
|
+
//console.log(window.performance.memory);
|
|
1096
|
+
|
|
1097
|
+
return new Promise((resolve) => {
|
|
1098
|
+
resolve(
|
|
1099
|
+
this.clear(null, true).then((resolve) => {
|
|
1100
|
+
this.map.remove();
|
|
1101
|
+
this.map = {};
|
|
1102
|
+
this.scene.remove(this.world);
|
|
1103
|
+
this.world.children = [];
|
|
1104
|
+
this.world = null;
|
|
1105
|
+
this.objectsCache.clear();
|
|
1106
|
+
this.labelRenderer.dispose();
|
|
1107
|
+
console.log(this.memory());
|
|
1108
|
+
this.renderer.dispose();
|
|
1109
|
+
return resolve;
|
|
1110
|
+
})
|
|
1111
|
+
);
|
|
1112
|
+
//console.log(window.performance.memory);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
},
|
|
1116
|
+
|
|
1117
|
+
defaultLights: function () {
|
|
1118
|
+
|
|
1119
|
+
this.lights.ambientLight = new THREE.AmbientLight(new THREE.Color('hsl(0, 0%, 100%)'), 0.75);
|
|
1120
|
+
this.scene.add(this.lights.ambientLight);
|
|
1121
|
+
|
|
1122
|
+
this.lights.dirLightBack = new THREE.DirectionalLight(new THREE.Color('hsl(0, 0%, 100%)'), 0.25);
|
|
1123
|
+
this.lights.dirLightBack.position.set(30, 100, 100);
|
|
1124
|
+
this.scene.add(this.lights.dirLightBack);
|
|
1125
|
+
|
|
1126
|
+
this.lights.dirLight = new THREE.DirectionalLight(new THREE.Color('hsl(0, 0%, 100%)'), 0.25);
|
|
1127
|
+
this.lights.dirLight.position.set(-30, 100, -100);
|
|
1128
|
+
this.scene.add(this.lights.dirLight);
|
|
1129
|
+
|
|
1130
|
+
},
|
|
1131
|
+
|
|
1132
|
+
realSunlight: function (helper = false) {
|
|
1133
|
+
|
|
1134
|
+
this.renderer.shadowMap.enabled = true;
|
|
1135
|
+
//this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
1136
|
+
this.lights.dirLight = new THREE.DirectionalLight(0xffffff, 1);
|
|
1137
|
+
this.scene.add(this.lights.dirLight);
|
|
1138
|
+
if (helper) {
|
|
1139
|
+
this.lights.dirLightHelper = new THREE.DirectionalLightHelper(this.lights.dirLight, 5);
|
|
1140
|
+
this.scene.add(this.lights.dirLightHelper);
|
|
1141
|
+
}
|
|
1142
|
+
let d2 = 1000; let r2 = 2; let mapSize2 = 8192;
|
|
1143
|
+
this.lights.dirLight.castShadow = true;
|
|
1144
|
+
this.lights.dirLight.shadow.radius = r2;
|
|
1145
|
+
this.lights.dirLight.shadow.mapSize.width = mapSize2;
|
|
1146
|
+
this.lights.dirLight.shadow.mapSize.height = mapSize2;
|
|
1147
|
+
this.lights.dirLight.shadow.camera.top = this.lights.dirLight.shadow.camera.right = d2;
|
|
1148
|
+
this.lights.dirLight.shadow.camera.bottom = this.lights.dirLight.shadow.camera.left = -d2;
|
|
1149
|
+
this.lights.dirLight.shadow.camera.near = 1;
|
|
1150
|
+
this.lights.dirLight.shadow.camera.visible = true;
|
|
1151
|
+
this.lights.dirLight.shadow.camera.far = 400000000;
|
|
1152
|
+
|
|
1153
|
+
this.lights.hemiLight = new THREE.HemisphereLight(new THREE.Color(0xffffff), new THREE.Color(0xffffff), 0.6);
|
|
1154
|
+
this.lights.hemiLight.color.setHSL(0.661, 0.96, 0.12);
|
|
1155
|
+
this.lights.hemiLight.groundColor.setHSL(0.11, 0.96, 0.14);
|
|
1156
|
+
this.lights.hemiLight.position.set(0, 0, 50);
|
|
1157
|
+
this.scene.add(this.lights.hemiLight);
|
|
1158
|
+
this.setSunlight();
|
|
1159
|
+
|
|
1160
|
+
this.map.once('idle', () => {
|
|
1161
|
+
this.setSunlight();
|
|
1162
|
+
this.repaint();
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
},
|
|
1166
|
+
|
|
1167
|
+
setDefaultView: function (options, defOptions) {
|
|
1168
|
+
options.bbox = (options.bbox || options.bbox == null) && defOptions.enableSelectingObjects;
|
|
1169
|
+
options.tooltip = (options.tooltip || options.tooltip == null) && defOptions.enableTooltips;
|
|
1170
|
+
options.mapScale = this.map.transform.scale;
|
|
1171
|
+
},
|
|
1172
|
+
|
|
1173
|
+
memory: function () { return this.renderer.info.memory },
|
|
1174
|
+
|
|
1175
|
+
programs: function () { return this.renderer.info.programs.length },
|
|
1176
|
+
|
|
1177
|
+
version: '2.2.7',
|
|
1178
|
+
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
var defaultOptions = {
|
|
1182
|
+
defaultLights: false,
|
|
1183
|
+
realSunlight: false,
|
|
1184
|
+
realSunlightHelper: false,
|
|
1185
|
+
passiveRendering: true,
|
|
1186
|
+
preserveDrawingBuffer: false,
|
|
1187
|
+
enableSelectingFeatures: false,
|
|
1188
|
+
enableSelectingObjects: false,
|
|
1189
|
+
enableDraggingObjects: false,
|
|
1190
|
+
enableRotatingObjects: false,
|
|
1191
|
+
enableTooltips: false,
|
|
1192
|
+
enableHelpTooltips: false,
|
|
1193
|
+
multiLayer: false,
|
|
1194
|
+
orthographic: false,
|
|
1195
|
+
fov: ThreeboxConstants.FOV_DEGREES,
|
|
1196
|
+
sky: false,
|
|
1197
|
+
terrain: false
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
export default Threebox;
|
|
1201
|
+
|