Skip to content

Commit 3ea0c37

Browse files
committed
Initial commit.
1 parent 296b6df commit 3ea0c37

5 files changed

Lines changed: 667 additions & 0 deletions

File tree

src/app.js

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
$(document).ready(function () {
2+
"use strict";
3+
4+
// Set your Bing Maps key to use when requesting Bing Maps resources.
5+
// Without your own key you will be using a limited WorldWind developer's key.
6+
// See: https://www.bingmapsportal.com/ to register for your own key and then enter it below
7+
const BING_API_KEY = "";
8+
9+
// Set the MapQuest API key used for the Nominatim service.
10+
// Get your own key at https://developer.mapquest.com/
11+
// Without your own key you will be using a limited WorldWind developer's key.
12+
const MAPQUEST_API_KEY = "";
13+
14+
/**
15+
* The Globe encapulates the WorldWindow object (wwd) and provides application
16+
* specific logic for interacting with layers.
17+
* @param {String} canvasId
18+
* @param {String|null} projectionName
19+
* @returns {Globe}
20+
*/
21+
class Globe {
22+
constructor(canvasId, projectionName) {
23+
// Create a WorldWindow globe on the specified HTML5 canvas
24+
this.wwd = new WorldWind.WorldWindow(canvasId);
25+
// Projection support
26+
this.roundGlobe = this.wwd.globe;
27+
this.flatGlobe = null;
28+
if (projectionName) {
29+
this.changeProjection(projectionName);
30+
}
31+
// Observables
32+
this.baseLayersLastUpdate = ko.observable(new Date());
33+
this.overlayLayersLastUpdate = ko.observable(new Date());
34+
this.settingLayersLastUpdate = ko.observable(new Date());
35+
// Add a BMNGOneImageLayer background layer. We're overriding the default
36+
// minimum altitude of the BMNGOneImageLayer so this layer always available.
37+
this.addLayer(new WorldWind.BMNGOneImageLayer(), {category: "background", minActiveAltitude: 0});
38+
}
39+
40+
get projectionNames() {
41+
return[
42+
"3D",
43+
"Equirectangular",
44+
"Mercator",
45+
"North Polar",
46+
"South Polar",
47+
"North UPS",
48+
"South UPS",
49+
"North Gnomonic",
50+
"South Gnomonic"
51+
];
52+
}
53+
54+
changeProjection(projectionName) {
55+
if (projectionName === "3D") {
56+
if (!this.roundGlobe) {
57+
this.roundGlobe = new WorldWind.Globe(new WorldWind.EarthElevationModel());
58+
}
59+
if (this.wwd.globe !== this.roundGlobe) {
60+
this.wwd.globe = this.roundGlobe;
61+
}
62+
} else {
63+
if (!this.flatGlobe) {
64+
this.flatGlobe = new WorldWind.Globe2D();
65+
}
66+
if (projectionName === "Equirectangular") {
67+
this.flatGlobe.projection = new WorldWind.ProjectionEquirectangular();
68+
} else if (projectionName === "Mercator") {
69+
this.flatGlobe.projection = new WorldWind.ProjectionMercator();
70+
} else if (projectionName === "North Polar") {
71+
this.flatGlobe.projection = new WorldWind.ProjectionPolarEquidistant("North");
72+
} else if (projectionName === "South Polar") {
73+
this.flatGlobe.projection = new WorldWind.ProjectionPolarEquidistant("South");
74+
} else if (projectionName === "North UPS") {
75+
this.flatGlobe.projection = new WorldWind.ProjectionUPS("North");
76+
} else if (projectionName === "South UPS") {
77+
this.flatGlobe.projection = new WorldWind.ProjectionUPS("South");
78+
} else if (projectionName === "North Gnomonic") {
79+
this.flatGlobe.projection = new WorldWind.ProjectionGnomonic("North");
80+
} else if (projectionName === "South Gnomonic") {
81+
this.flatGlobe.projection = new WorldWind.ProjectionGnomonic("South");
82+
}
83+
if (this.wwd.globe !== this.flatGlobe) {
84+
this.wwd.globe = this.flatGlobe;
85+
}
86+
}
87+
}
88+
89+
getLayers(category) {
90+
return this.wwd.layers.filter(layer => layer.category === category);
91+
}
92+
93+
/**
94+
* Add a layer to the globe and applies options object properties to the
95+
* the layer.
96+
* @param {WorldWind.Layer} layer
97+
* @param {Object|null} options E.g., {category: "base", enabled: true}
98+
*/
99+
addLayer(layer, options) {
100+
// Copy all properties defined on the options object to the layer
101+
if (options) {
102+
for (let prop in options) {
103+
if (!options.hasOwnProperty(prop)) {
104+
continue; // skip inherited props
105+
}
106+
layer[prop] = options[prop];
107+
}
108+
}
109+
// Assign a category property for layer management
110+
if (typeof layer.category === 'undefined') {
111+
layer.category = 'overlay'; // default category
112+
}
113+
114+
// Assign a unique layer ID to ease layer management
115+
layer.uniqueId = this.nextLayerId++;
116+
117+
// Add the layer to the globe
118+
this.wwd.addLayer(layer);
119+
120+
// Notify observers of a change
121+
this.updateLayers(layer.category);
122+
}
123+
124+
toggleLayer(layer) {
125+
// Apply rule: only one "base" layer can be enabled at a time
126+
if (layer.category === 'base') {
127+
this.wwd.layers.forEach(function (item) {
128+
if (item.category === 'base' && item !== layer) {
129+
item.enabled = false;
130+
}
131+
});
132+
}
133+
// Toggle the selected layer's visibility
134+
layer.enabled = !layer.enabled;
135+
// Trigger a redraw so the globe shows the new layer state ASAP
136+
this.wwd.redraw();
137+
138+
// Notify observers of a change
139+
this.updateLayers(layer.category);
140+
}
141+
142+
updateLayers(category) {
143+
const timestamp = new Date();
144+
switch (category) {
145+
case 'base':
146+
this.baseLayersLastUpdate(timestamp);
147+
break;
148+
case 'overlay':
149+
this.overlayLayersLastUpdate(timestamp);
150+
break;
151+
case 'setting':
152+
this.settingLayersLastUpdate(timestamp);
153+
break;
154+
default:
155+
}
156+
}
157+
}
158+
159+
function LayersViewModel(globe) {
160+
var self = this;
161+
self.globe = globe;
162+
self.baseLayers = ko.observableArray(globe.getLayers('base').reverse());
163+
self.overlayLayers = ko.observableArray(globe.getLayers('overlay').reverse());
164+
165+
// Update the view model whenever the model changes
166+
globe.baseLayersLastUpdate.subscribe(lastUpdated =>
167+
self.loadLayers(globe.getLayers('base'), self.baseLayers));
168+
globe.overlayLayersLastUpdate.subscribe(lastUpdated =>
169+
self.loadLayers(globe.getLayers('overlay'), self.overlayLayers));
170+
171+
self.loadLayers = function (layers, observableArray) {
172+
observableArray.removeAll();
173+
layers.reverse().forEach(layer => observableArray.push(layer));
174+
};
175+
176+
self.toggleLayer = function (layer) {
177+
self.globe.toggleLayer(layer);
178+
};
179+
180+
}
181+
182+
function SettingsViewModel(globe) {
183+
var self = this;
184+
self.globe = globe;
185+
self.settingLayers = ko.observableArray(globe.getLayers('setting').reverse());
186+
187+
// Update the view model whenever the model changes
188+
globe.settingLayersLastUpdate.subscribe(lastUpdated =>
189+
self.loadLayers(globe.getLayers('setting'), self.settingLayers));
190+
191+
self.loadLayers = function (layers, observableArray) {
192+
observableArray.removeAll();
193+
layers.reverse().forEach(layer => observableArray.push(layer));
194+
};
195+
196+
self.toggleLayer = function (layer) {
197+
self.globe.toggleLayer(layer);
198+
};
199+
}
200+
201+
/**
202+
* Define the view model for the Search. Uses the MapQuest Nominatim API.
203+
* Requires an access key. See: https://developer.mapquest.com/
204+
* @param {Globe} globe
205+
* @param {Function} preview Function to preview the results
206+
* @returns {SearchViewModel}
207+
*/
208+
function SearchViewModel(globe, preview) {
209+
var self = this;
210+
self.geocoder = new WorldWind.NominatimGeocoder();
211+
self.searchText = ko.observable('');
212+
self.performSearch = function () {
213+
if (!MAPQUEST_API_KEY) {
214+
console.error("SearchViewModel: A MapQuest API key is required to use the geocoder in production. Get your API key at https://developer.mapquest.com/");
215+
}
216+
// Get the value from the observable
217+
let queryString = self.searchText();
218+
if (queryString) {
219+
if (queryString.match(WorldWind.WWUtil.latLonRegex)) {
220+
// Treat the text as a lat, lon pair
221+
let tokens = queryString.split(",");
222+
let latitude = parseFloat(tokens[0]);
223+
let longitude = parseFloat(tokens[1]);
224+
// Center the globe on the lat, lon
225+
globe.goTo(new WorldWind.Location(latitude, longitude));
226+
} else {
227+
// Treat the text as an address or place name
228+
self.geocoder.lookup(queryString, function (geocoder, results) {
229+
if (results.length > 0) {
230+
// Open the modal dialog to preview and select a result
231+
preview(results);
232+
}
233+
}, MAPQUEST_API_KEY);
234+
}
235+
}
236+
};
237+
}
238+
239+
/**
240+
* Define the view model for the Search Preview.
241+
* @param {WorldWindow} primaryGlobe
242+
* @returns {PreviewViewModel}
243+
*/
244+
function PreviewViewModel(primaryGlobe) {
245+
var self = this;
246+
// Show a warning message about the MapQuest API key if missing
247+
this.showApiWarning = (MAPQUEST_API_KEY === null || MAPQUEST_API_KEY === "");
248+
// Create secondary globe with a 2D Mercator projection for the preview
249+
this.previewGlobe = new Globe("preview-canvas", "Mercator");
250+
let resultsLayer = new WorldWind.RenderableLayer("Results");
251+
let bingMapsLayer = new WorldWind.BingRoadsLayer();
252+
bingMapsLayer.detailControl = 1.25; // Show next level-of-detail sooner. Default is 1.75
253+
this.previewGlobe.addLayer(bingMapsLayer);
254+
this.previewGlobe.addLayer(resultsLayer);
255+
// Set up the common placemark attributes for the results
256+
let placemarkAttributes = new WorldWind.PlacemarkAttributes(null);
257+
placemarkAttributes.imageSource = WorldWind.configuration.baseUrl + "images/pushpins/castshadow-red.png";
258+
placemarkAttributes.imageScale = 0.5;
259+
placemarkAttributes.imageOffset = new WorldWind.Offset(
260+
WorldWind.OFFSET_FRACTION, 0.3,
261+
WorldWind.OFFSET_FRACTION, 0.0);
262+
// Create an observable array who's contents are displayed in the preview
263+
this.searchResults = ko.observableArray();
264+
this.selected = ko.observable();
265+
// Shows the given search results in a table with a preview globe/map
266+
this.previewResults = function (results) {
267+
if (results.length === 0) {
268+
return;
269+
}
270+
// Clear the previous results
271+
self.searchResults.removeAll();
272+
resultsLayer.removeAllRenderables();
273+
// Add the results to the observable array
274+
results.map(item => self.searchResults.push(item));
275+
// Create a simple placemark for each result
276+
for (let i = 0, max = results.length; i < max; i++) {
277+
let item = results[i];
278+
let placemark = new WorldWind.Placemark(
279+
new WorldWind.Position(
280+
parseFloat(item.lat),
281+
parseFloat(item.lon), 100));
282+
placemark.altitudeMode = WorldWind.RELATIVE_TO_GROUND;
283+
placemark.displayName = item.display_name;
284+
placemark.attributes = placemarkAttributes;
285+
resultsLayer.addRenderable(placemark);
286+
}
287+
288+
// Initialize preview with the first item
289+
self.previewSelection(results[0]);
290+
// Display the preview dialog
291+
$('#previewDialog').modal();
292+
$('#previewDialog .modal-body-table').scrollTop(0);
293+
};
294+
this.previewSelection = function (selection) {
295+
let latitude = parseFloat(selection.lat),
296+
longitude = parseFloat(selection.lon),
297+
location = new WorldWind.Location(latitude, longitude);
298+
// Update our observable holding the selected location
299+
self.selected(location);
300+
// Go to the posiion
301+
self.previewGlobe.wwd.goTo(location);
302+
};
303+
this.gotoSelected = function () {
304+
// Go to the location held in the selected observable
305+
primaryGlobe.wwd.goTo(self.selected());
306+
};
307+
}
308+
309+
// ---------------------
310+
// Construct our web app
311+
// ----------------------
312+
313+
// Initialize WorldWind properties before creating the first WorldWindow
314+
if (BING_API_KEY === null || BING_API_KEY === "") {
315+
// Warning: using a limited WorldWind developer's key for Bing resources.
316+
} else {
317+
WorldWind.BingMapsKey = BING_API_KEY;
318+
}
319+
320+
// Create the primary globe
321+
let globe = new Globe("globe-canvas");
322+
// Add layers ordered by drawing order: first to last
323+
globe.addLayer(new WorldWind.BMNGLayer(), {category: "base"});
324+
globe.addLayer(new WorldWind.BMNGLandsatLayer(), {category: "base", enabled: false});
325+
globe.addLayer(new WorldWind.BingAerialLayer(), {category: "base", enabled: false});
326+
globe.addLayer(new WorldWind.BingAerialWithLabelsLayer(), {category: "base", enabled: false, detailControl: 1.5});
327+
globe.addLayer(new WorldWind.BingRoadsLayer(), {category: "overlay", enabled: false, detailControl: 1.5, opacity: 0.75});
328+
globe.addLayer(new WorldWind.CoordinatesDisplayLayer(globe.wwd), {category: "setting"});
329+
globe.addLayer(new WorldWind.ViewControlsLayer(globe.wwd), {category: "setting"});
330+
globe.addLayer(new WorldWind.CompassLayer(), {category: "setting", enabled: false});
331+
332+
// Activate the Knockout bindings between our view models and the html
333+
this.layers = new LayersViewModel(globe);
334+
this.settings = new SettingsViewModel(globe);
335+
this.preview = new PreviewViewModel(globe);
336+
this.search = new SearchViewModel(globe, this.preview.previewResults);
337+
ko.applyBindings(this.search, document.getElementById('search'));
338+
ko.applyBindings(this.preview, document.getElementById('preview'));
339+
ko.applyBindings(this.layers, document.getElementById('layers'));
340+
ko.applyBindings(this.settings, document.getElementById('settings'));
341+
// Auto-collapse the main menu when its button items are clicked
342+
$('.navbar-collapse a[role="button"]').click(function () {
343+
$('.navbar-collapse').collapse('hide');
344+
});
345+
// Collapse card ancestors when the close icon is clicked
346+
$('.collapse .close').on('click', function () {
347+
$(this).closest('.collapse').collapse('hide');
348+
});
349+
});

0 commit comments

Comments
 (0)