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