Skip to content

Commit bc8e4ed

Browse files
committed
Merge develop
2 parents c285335 + 09441c3 commit bc8e4ed

File tree

10 files changed

+1008
-718
lines changed

10 files changed

+1008
-718
lines changed

Globe.js

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright 2018 Bruce Schubert.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
/*global $, WorldWind */
26+
27+
/**
28+
* The Globe encapulates the WorldWindow object (wwd) and provides application
29+
* specific logic for interacting with layers.
30+
*/
31+
export default class Globe {
32+
/**
33+
* Constructs a Globe object for the given canvas with an optional projection.
34+
* @param {String} canvasId
35+
* @param {String|null} projectionName
36+
* @returns {Globe}
37+
*/
38+
constructor(canvasId, projectionName) {
39+
// Create a WorldWindow globe on the specified HTML5 canvas
40+
this.wwd = new WorldWind.WorldWindow(canvasId);
41+
42+
// Layer management support
43+
this.nextLayerId = 1;
44+
45+
// Projection support
46+
this.roundGlobe = this.wwd.globe;
47+
this.flatGlobe = null;
48+
if (projectionName) {
49+
this.changeProjection(projectionName);
50+
}
51+
52+
// A map of category and 'observable' timestamp pairs
53+
this.categoryTimestamps = new Map();
54+
// Add a BMNGOneImageLayer background layer. We're overriding the default
55+
// minimum altitude of the BMNGOneImageLayer so this layer always available.
56+
this.addLayer(new WorldWind.BMNGOneImageLayer(), {category: "background", minActiveAltitude: 0});
57+
}
58+
59+
get projectionNames() {
60+
return[
61+
"3D",
62+
"Equirectangular",
63+
"Mercator",
64+
"North Polar",
65+
"South Polar",
66+
"North UPS",
67+
"South UPS",
68+
"North Gnomonic",
69+
"South Gnomonic"
70+
];
71+
}
72+
73+
changeProjection(projectionName) {
74+
if (projectionName === "3D") {
75+
if (!this.roundGlobe) {
76+
this.roundGlobe = new WorldWind.Globe(new WorldWind.EarthElevationModel());
77+
}
78+
if (this.wwd.globe !== this.roundGlobe) {
79+
this.wwd.globe = this.roundGlobe;
80+
}
81+
} else {
82+
if (!this.flatGlobe) {
83+
this.flatGlobe = new WorldWind.Globe2D();
84+
}
85+
if (projectionName === "Equirectangular") {
86+
this.flatGlobe.projection = new WorldWind.ProjectionEquirectangular();
87+
} else if (projectionName === "Mercator") {
88+
this.flatGlobe.projection = new WorldWind.ProjectionMercator();
89+
} else if (projectionName === "North Polar") {
90+
this.flatGlobe.projection = new WorldWind.ProjectionPolarEquidistant("North");
91+
} else if (projectionName === "South Polar") {
92+
this.flatGlobe.projection = new WorldWind.ProjectionPolarEquidistant("South");
93+
} else if (projectionName === "North UPS") {
94+
this.flatGlobe.projection = new WorldWind.ProjectionUPS("North");
95+
} else if (projectionName === "South UPS") {
96+
this.flatGlobe.projection = new WorldWind.ProjectionUPS("South");
97+
} else if (projectionName === "North Gnomonic") {
98+
this.flatGlobe.projection = new WorldWind.ProjectionGnomonic("North");
99+
} else if (projectionName === "South Gnomonic") {
100+
this.flatGlobe.projection = new WorldWind.ProjectionGnomonic("South");
101+
}
102+
if (this.wwd.globe !== this.flatGlobe) {
103+
this.wwd.globe = this.flatGlobe;
104+
}
105+
}
106+
}
107+
108+
/**
109+
* Returns a new array of layers within the given category.
110+
* @param {String} category E.g., "base", "overlay" or "setting".
111+
* @returns {Array}
112+
*/
113+
getLayers(category) {
114+
return this.wwd.layers.filter(layer => layer.category === category);
115+
}
116+
117+
/**
118+
* Add a layer to the globe and applies options object properties to the
119+
* the layer.
120+
* @param {WorldWind.Layer} layer
121+
* @param {Object|null} options E.g., {category: "base", enabled: true}
122+
*/
123+
addLayer(layer, options) {
124+
// Copy all properties defined on the options object to the layer object
125+
if (options) {
126+
for (let prop in options) {
127+
if (!options.hasOwnProperty(prop)) {
128+
continue; // skip inherited props
129+
}
130+
layer[prop] = options[prop];
131+
}
132+
}
133+
// Assign a category property for layer management
134+
if (typeof layer.category === 'undefined') {
135+
layer.category = 'overlay'; // default category
136+
}
137+
138+
// Assign a unique layer ID to ease layer management
139+
layer.uniqueId = this.nextLayerId++;
140+
141+
// Insert the layer within the given category
142+
// Find the index of first layer within the layer's category.
143+
let index = this.wwd.layers.findIndex(function (element) {
144+
return element.category === layer.category;
145+
});
146+
if (index < 0) {
147+
// Add to the end of the overall layer list
148+
this.wwd.addLayer(layer);
149+
} else {
150+
// Add the layer to the end the category
151+
let numLayers = this.getLayers(layer.category).length;
152+
this.wwd.insertLayer(index + numLayers, layer);
153+
}
154+
// Signal a change in the category
155+
this.updateCategoryTimestamp(layer.category);
156+
}
157+
158+
/**
159+
* Add a WMS layer to the globe and applies options object properties to the
160+
* the layer.
161+
* @param {String} serviceAddress Service address for the WMS map server
162+
* @param {String} layerName Layer name (not title) as defined in the capabilities document
163+
* @param {Object|null} options Options applied after loading, e.g., displayName and opacity
164+
*/
165+
addLayerFromWms(serviceAddress, layerName, options) {
166+
const self = this;
167+
168+
// Create a GetCapabilities request URL
169+
let url = serviceAddress.split('?')[0];
170+
url += "?service=wms";
171+
url += "&request=getcapabilities";
172+
let parseCapabilities = function (xml) {
173+
// Create a WmsCapabilities object from the returned xml
174+
var wmsCapabilities = new WorldWind.WmsCapabilities(xml);
175+
var layerForDisplay = wmsCapabilities.getNamedLayer(layerName);
176+
var layerConfig = WorldWind.WmsLayer.formLayerConfiguration(layerForDisplay);
177+
// Create the layer and add it to the globe
178+
var wmsLayer = new WorldWind.WmsLayer(layerConfig);
179+
// Extract the bbox out of the WMS layer configuration
180+
options.bbox = layerConfig.sector;
181+
// Add the layer to the globe
182+
self.addLayer(wmsLayer, options);
183+
};
184+
185+
// Create a request to retrieve the data
186+
let xhr = new XMLHttpRequest();
187+
xhr.open("GET", url, true); // performing an asynchronous request
188+
xhr.onreadystatechange = function () {
189+
if (xhr.readyState === XMLHttpRequest.DONE) {
190+
if (xhr.status === 200) {
191+
parseCapabilities(xhr.responseXML);
192+
} else {
193+
alert("XMLHttpRequest to " + url + " failed with status code " + xhr.status);
194+
}
195+
}
196+
};
197+
xhr.send();
198+
}
199+
200+
/**
201+
* Toggles the enabled state of the given layer and updates the layer
202+
* catetory timestamp. Applies a rule to the 'base' layers the ensures
203+
* only one base layer is enabled.
204+
* @param {WorldWind.Layer} layer
205+
*/
206+
toggleLayer(layer) {
207+
// Apply rule: only one "base" layer can be enabled at a time
208+
if (layer.category === 'base') {
209+
this.wwd.layers.forEach(function (item) {
210+
if (item.category === 'base' && item !== layer) {
211+
item.enabled = false;
212+
}
213+
});
214+
}
215+
// Toggle the selected layer's visibility
216+
layer.enabled = !layer.enabled;
217+
// Trigger a redraw so the globe shows the new layer state ASAP
218+
this.wwd.redraw();
219+
// Signal a change in the category
220+
this.updateCategoryTimestamp(layer.category);
221+
}
222+
223+
/**
224+
* Returns an observable containing the last update timestamp for the category.
225+
* @param {String} category
226+
* @returns {Observable}
227+
*/
228+
getCategoryTimestamp(category) {
229+
if (!this.categoryTimestamps.has(category)) {
230+
this.categoryTimestamps.set(category, ko.observable());
231+
}
232+
return this.categoryTimestamps.get(category);
233+
}
234+
235+
/**
236+
* Updates the timestamp for the given category.
237+
* @param {String} category
238+
*/
239+
updateCategoryTimestamp(category) {
240+
let timestamp = this.getCategoryTimestamp(category);
241+
timestamp(new Date());
242+
}
243+
244+
/**
245+
* Returns the first layer with the given name.
246+
* @param {String} name
247+
* @returns {WorldWind.Layer|null}
248+
*/
249+
findLayerByName(name) {
250+
let layers = this.wwd.layers.filter(layer => layer.displayName === name);
251+
return layers.length > 0 ? layers[0] : null;
252+
}
253+
254+
/**
255+
* Moves the WorldWindow camera to the center coordinates of the layer, and then zooms in (or out)
256+
* to provide a view of the layer as complete as possible.
257+
* @param {WorldWind.Layer} layer The selected layer for zooming
258+
* TODO: Make this to work when Sector/Bounding box crosses the 180° meridian
259+
*/
260+
zoomToLayer(layer) {
261+
// Verify layer sector (bounding box in 2D terms) existence and
262+
// do not center the camera if layer covers the whole globe.
263+
let layerSector = layer.bbox;
264+
if (!layerSector) { // null or undefined.
265+
console.error("zoomToLayer: No Layer sector / bounding box undefined!");
266+
return;
267+
}
268+
// Comparing each boundary of the sector to verify layer global coverage.
269+
if (layerSector.maxLatitude >= 90 &&
270+
layerSector.minLatitude <= -90 &&
271+
layerSector.maxLongitude >= 180 &&
272+
layerSector.minLongitude <= -180) {
273+
console.log("zoomToLayer: The selected layer covers the full globe. No camera centering needed.");
274+
return;
275+
}
276+
// Obtain layer center
277+
let center = findLayerCenter(layerSector);
278+
let range = computeZoomRange(layerSector);
279+
let position = new WorldWind.Position(center.latitude, center.longitude, range);
280+
// Move camera to position
281+
this.wwd.goTo(position);
282+
// Classical formula to obtain middle point between two coordinates
283+
function findLayerCenter(layerSector) {
284+
var centerLatitude = (layerSector.maxLatitude + layerSector.minLatitude) / 2;
285+
var centerLongitude = (layerSector.maxLongitude + layerSector.minLongitude) / 2;
286+
var layerCenter = new WorldWind.Location(centerLatitude, centerLongitude);
287+
return layerCenter;
288+
}
289+
// Zoom level is obtained following this simple method: Calculate approx arc length of the
290+
// sectors' diagonal, and set that as the range (altitude) of the camera.
291+
function computeZoomRange(layerSector) {
292+
var verticalBoundary = layerSector.maxLatitude - layerSector.minLatitude;
293+
var horizontalBoundary = layerSector.maxLongitude - layerSector.minLongitude;
294+
// Calculate diagonal angle between boundaries (simple pythagoras formula, we don't need to
295+
// consider vectors or great circles).
296+
var diagonalAngle = Math.sqrt(Math.pow(verticalBoundary, 2) + Math.pow(horizontalBoundary, 2));
297+
// If the diagonal angle is equal or more than an hemisphere (180°) don't change zoom level.
298+
// Else, use the diagonal arc length as camera altitude.
299+
if (diagonalAngle >= 180) {
300+
return null;
301+
} else {
302+
// Gross approximation of longitude of arc in km
303+
// (assuming spherical Earth with radius of 6,371 km. Accuracy is not needed for this).
304+
var diagonalArcLength = (diagonalAngle / 360) * (2 * 3.1416 * 6371000);
305+
return diagonalArcLength;
306+
}
307+
}
308+
}
309+
/**
310+
* loadLayers is a utility function used by the view models to copy
311+
* layers into an observable array. The top-most layer is first in the
312+
* observable array.
313+
* @param {Array} layers
314+
* @param {ko.observableArray} observableArray
315+
*/
316+
static loadLayers(layers, observableArray) {
317+
observableArray.removeAll();
318+
// Reverse the order of the layers to the top-most layer is first
319+
layers.reverse().forEach(layer => observableArray.push(layer));
320+
}
321+
};

LayersViewModel.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright 2018 Bruce Schubert.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
import Globe from './Globe.js';
26+
27+
/* global ko, WorldWind */
28+
29+
export default class LayersViewModel {
30+
/**
31+
* Constructs a view model for the globe's layers.
32+
* @param {Globe} globe
33+
*/
34+
constructor(globe) {
35+
let self = this;
36+
37+
this.globe = globe;
38+
this.baseLayers = ko.observableArray(globe.getLayers('base').reverse());
39+
this.overlayLayers = ko.observableArray(globe.getLayers('overlay').reverse());
40+
41+
// Update the view model whenever the model changes
42+
globe.getCategoryTimestamp('base').subscribe(newValue =>
43+
Globe.loadLayers(globe.getLayers('base'), self.baseLayers));
44+
globe.getCategoryTimestamp('overlay').subscribe(newValue =>
45+
Globe.loadLayers(globe.getLayers('overlay'), self.overlayLayers));
46+
47+
// Button click event handler specified in index.html view
48+
this.toggleLayer = function (layer) {
49+
self.globe.toggleLayer(layer);
50+
// Zoom to the layer if it has a bbox assigned to it
51+
if (layer.enabled && layer.bbox) {
52+
self.globe.zoomToLayer(layer);
53+
}
54+
};
55+
}
56+
}
57+

0 commit comments

Comments
 (0)