Skip to content

Instantly share code, notes, and snippets.

@emxsys
Last active May 19, 2026 00:34
Show Gist options
  • Select an option

  • Save emxsys/85f0c07ac9caf494265392c6faed3eaa to your computer and use it in GitHub Desktop.

Select an option

Save emxsys/85f0c07ac9caf494265392c6faed3eaa to your computer and use it in GitHub Desktop.
A complete 3D virtual globe example - HTML, JavaScript and CSS - using ESA-NASA Web WorldWind, Bootstrap and KnockoutJS featuring a 3D globe view, 2D map projections, markers and place name finder. Simply download and open this HTML file in your browser to run the app, or see http://worldwind.earth/sample-app.html for a preview and write-up.
<!DOCTYPE html>
<html lang="en">
<!--
A sample framework for the ESA-NASA WebWorldWind web applications.
Author: Bruce Schubert
License: MIT
See: https://worldwind.arc.nasa.gov/web/
-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="ESA-NASA WebWorldWind application framework with Bootstrap and KnockoutJS by Bruce Schubert">
<meta name="author" content="Bruce Schubert">
<title>WebWorldWind Example App</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/slate/bootstrap.min.css" type="text/css" />
<style>
body {
padding-top: 50px;
/* Move the content down because we have a fixed navbar that is 50px tall */
}
@media (min-width: 768px) {
.sidebar-left {
position: fixed;
top: 51px;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding-top: 0px;
padding-left: 20px;
padding-right: 20px;
padding-bottom: 20px;
overflow-x: hidden;
overflow-y: auto;
border-right: 1px solid #eee;
}
}
@media (max-width: 767px) {
.section-heading .section-toggle:after {
font-family: 'Glyphicons Halflings';
content: "\e114";
float: none;
color: lightgrey;
/* symbol for opening sections */
}
.section-heading .section-toggle.collapsed:after {
content: "\e080";
/* symbol for closing sections */
}
a.section-toggle {
text-decoration: none;
}
}
.main {
padding: 0px;
padding-left: 15px;
padding-right: 15px;
padding-bottom: 15px;
}
@media (min-width: 768px) {
padding-left:20px;
padding-right:20px;
}
.section-heading {
}
.sub-header {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
cursor: default;
-webkit-user-select: none; /* Chrome all / Safari all */
-moz-user-select: none; /* Firefox all */
-ms-user-select: none; /* IE 10+ */
}
.section-body>ul {
padding-left: 0px;
}
/* Controls the style of enabled and disabled layers */
.list-group-item {
font-style: italic;
}
.list-group-item.active {
font-style: normal;
}
</style>
</head>
<body>
<!-- Main Menu NavBar -->
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
<!-- Branding and 'hamburger' menu -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#main-navbar" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="https://worldwind.arc.nasa.gov/web" target="_blank">ESA-NASA Web WorldWind</a>
</div>
<!-- Navigation links, Projections and Search Box -->
<div class="collapse navbar-collapse" id="main-navbar">
<ul class="nav navbar-nav" role="tablist">
<!-- Home -->
<li class="active">
<a href="#home" role="tab" data-toggle="tab">
<span class="glyphicon glyphicon-home visible-sm-block" aria-hidden="true" style="padding-top:4px; padding-bottom:4px"></span>
<span class="hidden-sm" aria-hidden="true">Home</span>
</a>
</li>
<!-- Layers -->
<li>
<a href="#layers" role="tab" data-toggle="tab">
<span class="glyphicon glyphicon-list visible-sm-block" aria-hidden="true" style="padding-top:4px; padding-bottom:4px"></span>
<span class="hidden-sm" aria-hidden="true">Layers</span>
</a>
</li>
<!-- Markers -->
<li>
<a href="#markers" role="tab" data-toggle="tab">
<span class="glyphicon glyphicon-map-marker visible-sm-block" aria-hidden="true" style="padding-top:4px; padding-bottom:4px"></span>
<span class="hidden-sm" aria-hidden="true">Markers</span>
</a>
</li>
</ul>
<!-- Search Box -->
<div>
<form id="search" class="navbar-form navbar-left" role="search">
<div class="form-group">
<input type="text" class="form-control" placeholder="Destination" id="searchText" data-bind="value: searchText, valueUpdate: 'keyup', event: {keypress: onEnter}" />
</div>
<button id="searchButton" class="btn btn-primary" type="submit" data-bind="click: $root.performSearch">Go To</button>
</form>
</div>
<ul class="nav navbar-nav navbar-right">
<!-- Projections dropdown -->
<li id="projections" class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span data-bind="text: currentProjection"></span> <span class="caret"></span></a>
<!-- Bind the list to the 'projections' observableArray -->
<ul id="projections-menu" class="dropdown-menu" data-bind="foreach: $root.projections">
<li data-bind="click: $root.changeProjection">
<a><span data-bind="text: $data"></span></a>
</li>
</ul>
</li>
</ul>
</div>
<!-- /.navbar-collapse -->
</div>
<!-- /.container-fluid -->
</nav>
<!-- /NavBar -->
<!-- Content -->
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<div id="left-sidebar" class="col-sm-4 col-md-3 sidebar-left tab-content">
<!-- Home tab -->
<div id="home" class="tab-pane active" role="tabpanel">
<div class="section-heading">
<h4 class="sub-header">
<span class="glyphicon glyphicon-home" aria-hidden="true" style="padding-right:5px;"></span>
Home
<a class="section-toggle" data-toggle="collapse" href="#home-body"></a>
</h4>
</div>
<div id="home-body" class="section-body collapse in">
<p>This is a 3D virtual globe application built from <a href="https://worldwind.arc.nasa.gov/web">ESA-NASA Web WorldWind</a> and the <a href="https://getbootstrap.com/docs/3.3/">Bootstrap</a> and <a href="http://knockoutjs.com/">KnockoutJS</a> libraries.</p>
<p>It features a 3D globe and 2D map projections, layer management, markers and a place name finder.
</p>
</div>
</div>
<!-- /Home -->
<!-- Layers tab -->
<div id="layers" class="tab-pane" role="tabpanel">
<div class="section-heading">
<h4 class="sub-header">
<span class="glyphicon glyphicon-list" aria-hidden="true" style="padding-right:5px;"></span>
Layers
<a class="section-toggle" data-toggle="collapse" href="#layers-body">
</a>
</h4>
</div>
<div id="layers-body" class="section-body collapse in">
<!-- layer buttons bound to 'layers' observableArray -->
<!-- the button's active class is bound to 'layerEnabled' observable -->
<div class="list-group" data-bind="foreach: layers">
<button class="list-group-item btn btn-block" data-bind="click: $root.toggleLayer, css: {active: $data.layerEnabled}">
<span data-bind="text: $data.displayName"></span>
</button>
</div>
</div>
</div>
<!-- /Layer -->
<!-- Markers tab -->
<div id="markers" class="tab-pane" role="tabpanel">
<div class="section-heading">
<h4 class="sub-header">
<span class="glyphicon glyphicon-map-marker" aria-hidden="true"
style="padding-right:5px;"></span>
Markers
<a class="section-toggle" data-toggle="collapse" href="#markers-body"></a>
</h4>
</div>
<div id="markers-body" class="section-body collapse in">
<!-- Bind the list to the 'markers' observableArray -->
<ul id="markers-list" data-bind="foreach: $root.markers">
<li class="btn-group btn-block btn-group-sm">
<!-- Goto Button -->
<button type="button" class="btn btn-default" data-bind="click: $root.gotoMarker">
<span><img width="16px" height="16px" data-bind="attr:{src: $data.attributes.imageSource}" /> </span>
<span data-bind="text: $data.label"></span>
</button>
<!-- Edit Button -->
<button type="button" class="btn btn-default glyphicon glyphicon-pencil" data-bind="click: $root.editMarker"></button>
<!-- Delete Button -->
<button type="button" class="btn btn-default glyphicon glyphicon-trash" data-bind="click: $root.removeMarker"></button>
</li>
</ul>
</div>
</div>
<!-- /Markers -->
</div>
<!-- /Sidebar -->
<!-- Main Content -->
<div id="globe" class="col-sm-8 col-sm-offset-4 col-md-9 col-md-offset-3 main" style="height: calc(100vh - 100px);">
<div class="row" style="height: 100%;">
<!-- Globe -->
<div class="col-md-12" style="height: 100%;">
<div class="section-heading" style="width: 100%">
<h4 class="sub-header">
<span class="glyphicon glyphicon-globe" aria-hidden="true" style="padding-right:5px;"></span>
Globe
<!-- Add Marker button and palette -->
<span class="btn-group" style="float: right">
<button type="button"
class="btn btn-primary btn-sm"
style="padding-top: 0; padding-bottom: 0;"
data-bind="click: $root.addMarker">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<img width="28px" height="28px;"
data-bind="attr:{src: $root.selectedMarkerImage}"/>
</button>
<button type="button"
class="btn btn-primary btn-sm dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Markers Dropdown</span>
</button>
<!-- Bind the list to the 'markerPalette' observableArray -->
<ul id="marker-palette"
class="dropdown-menu"
style="left: initial; right: 0; min-width: 30px"
data-bind="foreach: markerPalette">
<li data-bind="click: $root.selectedMarkerImage">
<a><img width="28px" height="28px;" data-bind="attr:{src: $data}"/></a>
</li>
</ul>
</span>
</h4>
</div>
<!-- NASA Web World Wind -->
<canvas id="canvasOne" style="width: 100%; height: 100%;
background-color: rgb(36,74,101);
border:1px solid #000000;" data-bind="style: { cursor: dropIsArmed() ? 'crosshair' : 'default' }">
Try Chrome or FireFox.
</canvas>
<!-- NASA Web World Wind -->
</div>
</div>
</div>
</div>
</div>
<!-- Edit Marker Modal -->
<div class="modal fade" id="editMarkerModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="myModalLabel">Edit Marker</h4>
</div>
<div class="modal-body">
<div class="input-group">
<!-- <span class="input-group-addon" id="name-addon">Name</span>
<input type="text" class="form-control" id="marker-name" aria-describedby="name-addon">
-->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
<!-- Libraries -->
<script src="https://code.jquery.com/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://files.worldwind.arc.nasa.gov/artifactory/web/0.9.0/worldwind.min.js"></script>
<!-- Custom auto-expand/collapse behaviors -->
<script language="javascript">
// Auto-expand section-bodies when not small
$(window).resize(function () {
if ($(window).width() >= 768) {
$('.section-body').collapse('show');
}
});
// Auto-collapse navbar when tab items are clicked
$('.navbar-collapse a[role="tab"]').click(function () {
$(".navbar-collapse").collapse('hide');
});
// Auto-scroll-into-view expanded dropdown menus
$('.dropdown').on('shown.bs.dropdown', function (event) {
event.target.scrollIntoView(false); // align to bottom
});
$(document).ready(function () {
"use strict";
WorldWind.Logger.setLoggingLevel(WorldWind.Logger.LEVEL_WARNING);
// ----------------
// Setup the globe
// ----------------
// Attach the WorldWindow globe to the HTML5 canvas
var wwd = new WorldWind.WorldWindow("canvasOne"),
MARKERS = "Markers",
markersViewModel,
globeViewModel;
/**
* Returns the first layer with the given name.
* @param {String} name
* @returns {WorldWind.Layer}
*/
function findLayerByName(name) {
var layers = wwd.layers.filter(function (layer) {
return layer.displayName === MARKERS;
});
return layers[0];
}
// ------------------------------------
// Define a view model for the globe
// ------------------------------------
function GlobeViewModel(wwd, markersViewModel) {
var self = this;
// Marker icons used in the marker palette
self.markerPalette = ko.observableArray([
"https://files.worldwind.arc.nasa.gov/artifactory/web/0.9.0/images/pushpins/castshadow-red.png",
"https://files.worldwind.arc.nasa.gov/artifactory/web/0.9.0/images/pushpins/castshadow-green.png",
"https://files.worldwind.arc.nasa.gov/artifactory/web/0.9.0/images/pushpins/castshadow-blue.png",
"https://files.worldwind.arc.nasa.gov/artifactory/web/0.9.0/images/pushpins/castshadow-orange.png",
"https://files.worldwind.arc.nasa.gov/artifactory/web/0.9.0/images/pushpins/castshadow-teal.png",
"https://files.worldwind.arc.nasa.gov/artifactory/web/0.9.0/images/pushpins/castshadow-purple.png",
"https://files.worldwind.arc.nasa.gov/artifactory/web/0.9.0/images/pushpins/castshadow-white.png",
"https://files.worldwind.arc.nasa.gov/artifactory/web/0.9.0/images/pushpins/castshadow-black.png"
]);
// The currently selected marker icon
self.selectedMarkerImage = ko.observable(self.markerPalette()[0]);
// Used for cursor style and click handling
self.dropIsArmed = ko.observable(false);
//The dropCallback is supplied with the click position and the dropObject.
self.dropCallback = null;
// The object passed to the dropCallback
self.dropObject = null;
/**
* Adds a marker to the globe.
*/
self.addMarker = function () {
self.dropIsArmed(true);
self.dropCallback = markersViewModel.dropMarkerCallback;
self.dropObject = self.selectedMarkerImage();
};
// Invoke addMarker when an image is selected from the palette
self.selectedMarkerImage.subscribe(self.addMarker);
/**
* Handles a click on the WorldWindow. If a "drop" action callback has been
* defined, it invokes the function with the picked location.
* @param {Object} event
*/
self.handleClick = function (event) {
if (!self.dropIsArmed()) {
return;
}
var type = event.type,
x, y,
pickList,
terrain;
// Get the clicked window coords
switch (type) {
case 'click':
x = event.clientX;
y = event.clientY;
break;
case 'touchend':
if (!event.changedTouches[0]) {
return;
}
x = event.changedTouches[0].clientX;
y = event.changedTouches[0].clientY;
break;
}
if (self.dropCallback) {
// Get all the picked items
pickList = wwd.pickTerrain(wwd.canvasCoordinates(x, y));
// Terrain should be one of the items if the globe was clicked
terrain = pickList.terrainObject();
if (terrain) {
self.dropCallback(terrain.position, self.dropObject);
}
}
self.dropIsArmed(false);
event.stopImmediatePropagation();
};
// Assign the click handler to the WorldWind
wwd.addEventListener('click', function (event) {
self.handleClick(event);
});
}
// ------------------------------------
// Define a view model for the layers
// ------------------------------------
function LayersViewModel(wwd) {
var self = this;
// WMS configurations for USGS WMS layers
// See: https://basemap.nationalmap.gov/arcgis/rest/services/
// See: https://services.nationalmap.gov/arcgis/rest/services/
var usgsTopoCfg = {
title: "USGS Topo Basemap",
version: "1.3.0",
service: "https://basemap.nationalmap.gov/arcgis/services/USGSTopo/MapServer/WmsServer?",
layerNames: "0",
sector: new WorldWind.Sector(-89.0, 89.0, -180, 180),
levelZeroDelta: new WorldWind.Location(36, 36),
numLevels: 13, // capabilites says 16, but really 13. limit to prevent requests for blank tiles
format: "image/png",
size: 256,
coordinateSystem: "EPSG:4326", // optional
styleNames: "" // (optional) A comma separated list of the styles to include
},
usgsImageryTopoCfg = {
title: "USGS Imagery Topo Basemap",
version: "1.3.0",
service: "https://basemap.nationalmap.gov/arcgis/services/USGSImageryTopo/MapServer/WmsServer?",
layerNames: "0",
sector: new WorldWind.Sector(-89.0, 89.0, -180, 180),
levelZeroDelta: new WorldWind.Location(36, 36),
numLevels: 12,
format: "image/png",
size: 256,
coordinateSystem: "EPSG:4326", // optional
styleNames: "" //A comma separated list of the styles
},
usgsContoursCfg = {
title: "USGS Contour Lines Overlay",
version: "1.3.0",
service: "https://services.nationalmap.gov/arcgis/services/Contours/MapServer/WmsServer?",
//layerNames: "1,2,4,5,7,8", // lines and labels: large scale, 50' and 100' respectively
layerNames: "10,11,12,14,15,16,18,19", // lines and labels: large scale, 50' and 100' respectively
sector: new WorldWind.Sector(18.915561901, 64.8750000000001, -160.544024274, -66.9502505149999),
levelZeroDelta: new WorldWind.Location(36, 36),
numLevels: 19,
format: "image/png",
size: 256,
coordinateSystem: "EPSG:4326", // optional
styleNames: "" // A comma separated list of the styles
};
// Define our layers and layer options
var layer,
layerCfg = [{
layer: new WorldWind.BMNGLayer(),
enabled: true
}, {
layer: new WorldWind.BMNGLandsatLayer(),
enabled: true
}, {
layer: new WorldWind.WmsLayer(usgsImageryTopoCfg),
enabled: false,
detailControl: 2.0,
disableTransparentColor: true // prevent white labels from being transparent
}, {
layer: new WorldWind.WmsLayer(usgsTopoCfg),
enabled: false,
disableTransparentColor: true // prevent white labels from being transparent
}, {
layer: new WorldWind.BingAerialLayer(),
enabled: false
}, {
layer: new WorldWind.BingAerialWithLabelsLayer(),
enabled: true
}, {
layer: new WorldWind.BingRoadsLayer(),
enabled: false
}, {
layer: new WorldWind.WmsLayer(usgsContoursCfg),
enabled: false,
opacity: 0.85
}, {
layer: new WorldWind.RenderableLayer("Markers"),
enabled: true
}, {
layer: new WorldWind.CompassLayer(),
enabled: true
}, {
layer: new WorldWind.CoordinatesDisplayLayer(wwd),
enabled: true
}, {
layer: new WorldWind.ViewControlsLayer(wwd),
enabled: true,
// Override the default placement from bottom-left to top-left
// Leave room at the top for the Coordinates output
placement: new WorldWind.Offset(
WorldWind.OFFSET_FRACTION, 0,
WorldWind.OFFSET_FRACTION, 1),
alignment: new WorldWind.Offset(
WorldWind.OFFSET_PIXELS, -10,
WorldWind.OFFSET_INSET_PIXELS, -18)
}];
// Apply the layer options and add the layers to the globe
for (var i = 0; i < layerCfg.length; i++) {
layer = layerCfg[i].layer;
layer.enabled = layerCfg[i].enabled;
layer.opacity = layerCfg[i].opacity ? layerCfg[i].opacity : 1.0;
if (layerCfg[i].placement) {
layer.placement = layerCfg[i].placement;
}
if (layerCfg[i].alignment) {
layer.alignment = layerCfg[i].alignment;
}
if (layerCfg[i].disableTransparentColor) {
layer.urlBuilder.transparent = false;
}
if (layerCfg[i].detailControl) {
layer.detailControl = layerCfg[i].detailControl;
}
// Set the layer's view model properties
layer.layerEnabled = ko.observable(layer.enabled);
// Add the layer to the globe
wwd.addLayer(layer);
}
// The layers collection view model
self.layers = ko.observableArray(wwd.layers);
// Layer item click handler
self.toggleLayer = function (layer) {
layer.enabled = !layer.enabled;
layer.layerEnabled(layer.enabled); // view model
wwd.redraw();
};
}
// -----------------------------------------
// Define a view model for the projections
// -----------------------------------------
function ProjectionsViewModel(wwd) {
var self = this;
self.projections = ko.observableArray([
"3D",
"Equirectangular",
"Mercator",
"North Polar",
"South Polar",
"North UPS",
"South UPS",
"North Gnomonic",
"South Gnomonic"
]);
// Projection support vars
self.roundGlobe = wwd.globe;
self.flatGlobe = null;
// Track the current projection
self.currentProjection = ko.observable('3D');
// Projection click handler
self.changeProjection = function (projectionName) {
// Capture the selection
self.currentProjection(projectionName);
// Change the projection
if (projectionName === "3D") {
if (!self.roundGlobe) {
self.roundGlobe = new WorldWind.Globe(new WorldWind.EarthElevationModel());
}
if (wwd.globe !== self.roundGlobe) {
wwd.globe = self.roundGlobe;
}
} else {
if (!self.flatGlobe) {
self.flatGlobe = new WorldWind.Globe2D();
}
if (projectionName === "Equirectangular") {
self.flatGlobe.projection = new WorldWind.ProjectionEquirectangular();
} else if (projectionName === "Mercator") {
self.flatGlobe.projection = new WorldWind.ProjectionMercator();
} else if (projectionName === "North Polar") {
self.flatGlobe.projection = new WorldWind.ProjectionPolarEquidistant("North");
} else if (projectionName === "South Polar") {
self.flatGlobe.projection = new WorldWind.ProjectionPolarEquidistant("South");
} else if (projectionName === "North UPS") {
self.flatGlobe.projection = new WorldWind.ProjectionUPS("North");
} else if (projectionName === "South UPS") {
self.flatGlobe.projection = new WorldWind.ProjectionUPS("South");
} else if (projectionName === "North Gnomonic") {
self.flatGlobe.projection = new WorldWind.ProjectionGnomonic("North");
} else if (projectionName === "South Gnomonic") {
self.flatGlobe.projection = new WorldWind.ProjectionGnomonic("South");
}
if (wwd.globe !== self.flatGlobe) {
wwd.globe = self.flatGlobe;
}
}
wwd.redraw();
};
}
// ---------------------------------------
// Define the view model for the SearchBox
// ---------------------------------------
function SearchViewModel(wwd) {
var self = this;
self.geocoder = new WorldWind.NominatimGeocoder();
self.goToAnimator = new WorldWind.GoToAnimator(wwd);
self.searchText = ko.observable('');
self.onEnter = function (data, event) {
if (event.keyCode === 13) {
self.performSearch();
}
return true;
};
self.performSearch = function () {
var queryString = self.searchText();
if (queryString) {
var latitude, longitude;
if (queryString.match(WorldWind.WWUtil.latLonRegex)) {
var tokens = queryString.split(",");
latitude = parseFloat(tokens[0]);
longitude = parseFloat(tokens[1]);
self.goToAnimator.goTo(new WorldWind.Location(latitude, longitude));
} else {
self.geocoder.lookup(queryString, function (geocoder, result) {
if (result.length > 0) {
latitude = parseFloat(result[0].lat);
longitude = parseFloat(result[0].lon);
self.goToAnimator.goTo(new WorldWind.Location(latitude, longitude));
}
});
}
}
};
}
// -------------------------------------
// Define a view model for the markers
// -------------------------------------
function MarkersViewModel(wwd) {
var self = this;
self.markers = ko.observableArray();
// Set up the common placemark attributes.
self.commonAttributes = new WorldWind.PlacemarkAttributes(null);
self.commonAttributes.imageScale = 1;
self.commonAttributes.imageOffset = new WorldWind.Offset(
WorldWind.OFFSET_FRACTION, 0.3,
WorldWind.OFFSET_FRACTION, 0.0);
self.commonAttributes.imageColor = WorldWind.Color.WHITE;
self.commonAttributes.labelAttributes.offset = new WorldWind.Offset(
WorldWind.OFFSET_FRACTION, 0.5,
WorldWind.OFFSET_FRACTION, 1.0);
self.commonAttributes.labelAttributes.color = WorldWind.Color.YELLOW;
self.commonAttributes.drawLeaderLine = true;
self.commonAttributes.leaderLineAttributes.outlineColor = WorldWind.Color.RED;
/**
* "Drop" action callback creates and adds a marker (WorldWind.Placemark) to the globe.
*
* @param {WorldWind.Location} position
* @param {type} imageSource
*/
self.dropMarkerCallback = function (position, imageSource) {
var attributes = new WorldWind.PlacemarkAttributes(self.commonAttributes),
placemark = new WorldWind.Placemark(position, /*eyeDistanceScaling*/true, attributes),
markerLayer = findLayerByName(MARKERS);
// Set the placemark properties and attributes
placemark.label = "Lat " + position.latitude.toPrecision(4).toString() + "\n" + "Lon " + position.longitude.toPrecision(5).toString();
placemark.altitudeMode = WorldWind.CLAMP_TO_GROUND;
placemark.eyeDistanceScalingThreshold = 2500000;
attributes.imageSource = imageSource;
// Add the placemark to the layer and to the observable array
markerLayer.addRenderable(placemark);
self.markers.push(placemark);
};
/** Animator used to programmatically move the globe to a marker */
self.goToAnimator = new WorldWind.GoToAnimator(wwd);
/**
* "Goto" function centers the globe on the given marker.
* @param {WorldWind.Placemark} marker
*/
self.gotoMarker = function (marker) {
self.goToAnimator.goTo(new WorldWind.Location(
marker.position.latitude,
marker.position.longitude));
};
/**
* "Edit" function invokes a modal dialog to edit the marker attributes.
* @param {WorldWind.Placemark} marker
*/
self.editMarker = function (marker) {
// TODO bind marker to dialog, maybe create an individual marker view-model
// var options = {};
// $('#editMarkerModal').modal(options)
};
/**
* "Remove" function removes a marker from the globe.
* @param {WorldWind.Placemark} marker
*/
self.removeMarker = function (marker) {
var markerLayer = findLayerByName(MARKERS),
i, max, placemark;
// Find and remove the marker from the layer and the observable array
for (i = 0, max = self.markers().length; i < max; i++) {
placemark = markerLayer.renderables[i];
if (placemark === marker) {
markerLayer.renderables.splice(i, 1);
self.markers.remove(marker);
break;
}
}
};
}
// --------------------------------------------------------
// Bing the view models to the corresponding HTML elements
// --------------------------------------------------------
markersViewModel = new MarkersViewModel(wwd);
globeViewModel = new GlobeViewModel(wwd, markersViewModel);
ko.applyBindings(new LayersViewModel(wwd), document.getElementById('layers'));
ko.applyBindings(new ProjectionsViewModel(wwd), document.getElementById('projections'));
ko.applyBindings(new SearchViewModel(wwd), document.getElementById('search'));
ko.applyBindings(markersViewModel, document.getElementById('markers'));
ko.applyBindings(globeViewModel, document.getElementById('globe'));
});
</script>
</body>
</html>
@BooLightning

Copy link
Copy Markdown

What should i use if i want to make a 3d globe?
I want to make a sort of open source globet that anybody can add too and i don't know what language to use or in what way.

@laurentlysmendy088-hue

Copy link
Copy Markdown

<title>ANM-2061 — Globe 3D Interactif (3 Points)</title> <style> :root { --bg-color: #0b0e14; --panel-bg: rgba(17, 22, 31, 0.85); --accent-color: #58a6ff; --text-color: #c9d1d9; --danger-color: #f85149; --grid-line: #21262d; }
    body {
        margin: 0;
        padding: 0;
        background-color: var(--bg-color);
        color: var(--text-color);
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
        overflow: hidden;
    }

    #canvas-container {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        z-index: 1;
        cursor: grab;
    }
    
    #canvas-container:active {
        cursor: grabbing;
    }

    .interface-overlay {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        z-index: 10;
        pointer-events: none;
        display: grid;
        grid-template-columns: 350px 1fr 380px;
        grid-template-rows: 60px 1fr;
        padding: 20px;
        box-sizing: border-box;
        gap: 20px;
    }

    header {
        grid-column: 1 / -1;
        background: var(--panel-bg);
        backdrop-filter: blur(12px);
        border: 1px solid #30363d;
        border-radius: 6px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 0 20px;
        pointer-events: auto;
    }

    h1 {
        margin: 0;
        font-size: 1.1rem;
        letter-spacing: 2px;
        color: #fff;
        font-weight: 600;
    }

    .panel {
        background: var(--panel-bg);
        backdrop-filter: blur(12px);
        border: 1px solid #30363d;
        border-radius: 6px;
        padding: 18px;
        pointer-events: auto;
        display: flex;
        flex-direction: column;
        gap: 15px;
        overflow-y: auto;
        box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
    }

    h3 {
        margin-top: 0;
        border-bottom: 1px solid var(--grid-line);
        padding-bottom: 8px;
        color: #fff;
        font-size: 1rem;
        letter-spacing: 0.5px;
    }

    .btn-download {
        background: #238636;
        color: white;
        border: none;
        padding: 8px 16px;
        border-radius: 6px;
        cursor: pointer;
        font-weight: bold;
        transition: background 0.2s;
    }

    .btn-download:hover {
        background: #2ea043;
    }

    table {
        width: 100%;
        border-collapse: collapse;
        font-size: 0.85rem;
    }

    th, td {
        padding: 10px 8px;
        text-align: left;
        border-bottom: 1px solid var(--grid-line);
    }

    th {
        color: var(--accent-color);
    }

    .badge-danger {
        background: rgba(248, 81, 73, 0.15);
        color: var(--danger-color);
        padding: 3px 8px;
        border-radius: 4px;
        font-weight: bold;
        border: 1px solid rgba(248, 81, 73, 0.3);
    }

    .nav-instructions {
        font-size: 0.75rem;
        color: #8b949e;
        background: rgba(0,0,0,0.4);
        padding: 4px 10px;
        border-radius: 20px;
        border: 1px solid #30363d;
    }
</style>
<!-- Chargement des dépendances graphiques et de navigation 3D -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<div id="canvas-container"></div>

<div class="interface-overlay">
    <header>
        <h1>ANM-2061 // AXE GLOBAL INTERACTIF</h1>
        <div class="nav-instructions">🖱️ Glisser : Rotation | 📜 Molette : Zoom</div>
        <button class="btn-download" onclick="downloadDashboard()">💾 Télécharger le Dashboard</button>
    </header>

    <!-- PANNEAU GAUCHE : SUIVI DES TROIS POINTS -->
    <div class="panel">
        <h3>🌐 Index de Tension Synchrone</h3>
        <table>
            <thead>
                <tr>
                    <th>Identifiant</th>
                    <th>Statut Matrix</th>
                    <th>Index</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td><strong>Point 1 (G4)</strong></td>
                    <td>Corrigé</td>
                    <td><span class="badge-danger">+2</span></td>
                </tr>
                <tr>
                    <td><strong>Point 2 (G5)</strong></td>
                    <td>Remplacé</td>
                    <td><span class="badge-danger">+2</span></td>
                </tr>
                <tr>
                    <td><strong>Point 3 (Axe)</strong></td>
                    <td>Synchronisé</td>
                    <td><span class="badge-danger">+2</span></td>
                </tr>
            </tbody>
        </table>
        <p style="font-size: 0.8rem; line-height: 1.4; color: #8b949e; margin: 0;">
            Les coordonnées géodésiques lient les trois composants à un niveau de risque équivalent. Utilisez le contrôle orbital pour inspecter la zone de convergence.
        </p>
    </div>

    <!-- ESPACE CENTRAL LIBRE POUR L'INTERACTION GLOBE -->
    <div></div>

    <!-- PANNEAU DROIT : CARTOGRAPHIE RADAR -->
    <div class="panel">
        <h3>📊 Profil Triangulaire du Risque</h3>
        <canvas id="radarChart3Points" style="max-height: 240px;"></canvas>
        
        <h3>📝 Notes d'Analyse</h3>
        <p style="font-size: 0.85rem; line-height: 1.5; margin: 0; text-align: justify;">
            L'égalisation stricte à <strong>+2</strong> configure un espace d'alerte homogène. L'interaction en temps réel permet d'évaluer visuellement les vecteurs systémiques qui relient les trois points sur la grille du modèle.
        </p>
    </div>
</div>

<script>
    // --- ARCHITECTURE 3D GLOBE INTERACTIF ---
    const container = document.getElementById('canvas-container');
    const scene = new THREE.Scene();
    
    const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0, 1.5, 4.5);

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    container.appendChild(renderer.domElement);

    // Initialisation d'OrbitControls
    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true; // Inertie fluide lors du relâchement
    controls.dampingFactor = 0.05;
    controls.enablePan = false;    // Reste centré sur le globe
    controls.minDistance = 2.5;    // Zoom maximum
    controls.maxDistance = 8.0;    // Dézoom maximum

    const globalGroup = new THREE.Group();
    scene.add(globalGroup);

    // Construction de la structure de la sphère (Hologramme fil de fer)
    const globeGeo = new THREE.SphereGeometry(1.8, 30, 30);
    const globeMat = new THREE.MeshBasicMaterial({
        color: 0x58a6ff,
        wireframe: true,
        transparent: true,
        opacity: 0.15
    });
    const globeMesh = new THREE.Mesh(globeGeo, globeMat);
    globalGroup.add(globeMesh);

    // Définition des coordonnées tridimensionnelles des 3 points pivots
    const p1 = new THREE.Vector3(1.0, 0.9, 1.1);
    const p2 = new THREE.Vector3(-1.2, 0.6, 1.1);
    const p3 = new THREE.Vector3(0.1, -1.1, 1.4);

    const pointGeo = new THREE.SphereGeometry(0.07, 16, 16);
    const pointMat = new THREE.MeshBasicMaterial({ color: 0xf85149 });

    // Positionnement des trois points d'alerte (+2)
    [p1, p2, p3].forEach((pos) => {
        const mesh = new THREE.Mesh(pointGeo, pointMat);
        mesh.position.copy(pos);
        globalGroup.add(mesh);
    });

    // Tracé des segments reliant les trois points
    const lineMat = new THREE.LineBasicMaterial({ color: 0xf85149, transparent: true, opacity: 0.45 });
    
    const drawVector = (start, end) => {
        const points = [start, end];
        const lineGeo = new THREE.BufferGeometry().setFromPoints(points);
        return new THREE.Line(lineGeo, lineMat);
    };

    globalGroup.add(drawVector(p1, p2));
    globalGroup.add(drawVector(p2, p3));
    globalGroup.add(drawVector(p3, p1));

    // Boucle de rendu et d'animation
    function animate() {
        requestAnimationFrame(animate);
        
        // Légère rotation continue automatique si l'utilisateur n'agit pas
        if (!controls.state == -1) {
            globalGroup.rotation.y += 0.0008;
        }
        
        controls.update(); 
        renderer.render(scene, camera);
    }
    animate();

    // Ajustement automatique lors du redimensionnement de la fenêtre
    window.addEventListener('resize', () => {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    });

    // --- ENREGISTREMENT DU GRAPHIQUE RADAR (CHART.JS) ---
    const ctx = document.getElementById('radarChart3Points').getContext('2d');
    new Chart(ctx, {
        type: 'radar',
        data: {
            labels: ['Point G4', 'Point G5', 'Pivot 3', 'Indice Périphérique', 'Stabilité Globale'],
            datasets: [{
                label: 'Niveau Critique',
                data: [2.0, 2.0, 2.0, 0.6, 0.3], // Visualisation de l'alignement à +2
                backgroundColor: 'rgba(248, 81, 73, 0.12)',
                borderColor: '#f85149',
                borderWidth: 2,
                pointBackgroundColor: '#ffffff'
            }]
        },
        options: {
            scales: {
                r: {
                    grid: { color: '#21262d' },
                    angleLines: { color: '#21262d' },
                    ticks: { display: false },
                    suggestMin: 0,
                    suggestMax: 2.2
                }
            },
            plugins: {
                legend: { display: false }
            }
        }
    });

    // --- MODULE D'EXPORTATION INSTANTANÉ ---
    function downloadDashboard() {
        const htmlContent = document.documentElement.outerHTML;
        const blob = new Blob([htmlContent], { type: 'text/html' });
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = 'ANM_2061_Globe_Interactif_3Points.html';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }
</script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment