This sample uses the device's orientation and CSS3 to build a basic compass.
Instances of the DeviceOrientationEvent class are fired while the device is changing the orientation. The DeviceOrientationEvent class encapsulates the angles of rotation (alpha, beta, and gamma) in degrees and heading. The angles of rotation do not represent the real world orientation. However, the webkitCompassHeading property can be used to track real-world changes. You can track changes in the direction the device is pointing by tracking the difference between the device's new heading and the previous heading.
var previousHeading = 0; // get the compass heading and device orientation var heading = e.webkitCompassHeading + window.orientation; // transform compassHousing.style.webkitTransition = 'all 0.3s ease-in-out'; // rotate the compass compassHousing.style.webkitTransform = 'rotateZ(-' + heading + 'deg)'; previousHeading = heading;
The webkitCompassHeadingAccuracy is used to indicate the accuracy of the compass data in degrees. For instance, if the value is 10, the heading is plus or minus 10. In this sample, a jewel is placed in the center of the compass face to indicate the accuracy of the current compass readings. As the compass reading becomes less accurate the jewel changes from green to red.
<!DOCTYPE html> <html> <head> <title>Compass</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <style> body { padding: 0; margin: 0; overflow: hidden; } #map { height: 100%; width: 100%; position: absolute; z-index: 1; } #compassHousing { margin-left: 5px; margin-top: 5px; background-color: #CCC; border-style: solid; border-width: 1px; border-radius: 62.5px; padding: 2px; position: absolute; z-index: 2; opacity: 0.77; -moz-box-shadow: 2px 3px 10px 2px #333; -webkit-box-shadow: 2px 3px 10px 2px #333; box-shadow: 2px 3px 10px 2px #333; } #compassFace { z-index: -1; position: absolute; } #compassNeedle { position: absolute; -webkit-transition-property: -webkit-transform; -webkit-transition-duration: .5s; -webkit-transition-timing-function: ease-out; } @-webkit-keyframes pulse { 0% { opacity: 1.0; } 45% { opacity: 0.20; } 100% { opacity: 1.0; } } @-moz-keyframes pulse { 0% { opacity: 1.0; } 45% { opacity: 0.20; } 100% { opacity: 1.0; } } #map_graphics_layer { -webkit-animation-duration: 3s; -webkit-animation-iteration-count: infinite; -webkit-animation-name: pulse; -moz-animation-duration: 3s; -moz-animation-iteration-count: infinite; -moz-animation-name: pulse; } /* compass */ @media(orientation: landscape) { #compass { margin-top: 20px; margin-left: 20px; opacity: 0.85; filter: alpha(opacity=85); position: absolute; z-index: 2; } } @media(orientation: portrait) { #compass { margin-top: 20px; margin-left: 20px; opacity: 0.85; filter: alpha(opacity=85); position: absolute; z-index: 2; } } </style> <link rel="stylesheet" href="https://js.arcgis.com/3.46/esri/css/esri.css" /> <script src="https://js.arcgis.com/3.46compact/"></script> <script> require([ "esri/Color", "dojo/dom", "dojo/dom-geometry", "dojo/has", "dojo/on", "dojo/parser", "dojo/ready", "dojo/window", "esri/geometry/Point", "esri/graphic", "esri/map", "esri/symbols/SimpleLineSymbol", "esri/symbols/SimpleMarkerSymbol" ], function(Color, dom, domGeom, has, on, parser, ready, win, Point, Graphic, Map, SimpleLineSymbol, SimpleMarkerSymbol) { var map; var COMPASS_SIZE = 125; var pt; var graphic; var watchId; var compassFaceRadius, compassFaceDiameter; var needleAngle, needleWidth, needleLength, compassRing; var renderingInterval = -1; var currentHeading; var hasCompass; var compassHousing; var containerX; var containerY; var compassNeedleContext; ready(function() { parser.parse(); var supportsOrientationChange = "onorientationchange" in window, orientationEvent = supportsOrientationChange ? "orientationchange" : "resize"; window.addEventListener(orientationEvent, function () { orientationChanged(); }, false); map = new Map("map", { basemap: "gray-vector", center: [-117.708, 33.523], zoom: 15, slider: false }); on(map, "load", mapLoadHandler); loadCompass(); }); // The HTML5 geolocation API is used to get the user's current position. function mapLoadHandler() { on(window, 'resize', map, map.resize); // check if geolocaiton is supported if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(zoomToLocation, locationError); // retrieve update about the current geographic location of the device watchId = navigator.geolocation.watchPosition(showLocation, locationError); } else { alert("Browser doesn't support Geolocation. Visit http://caniuse.com to discover browser support for the Geolocation API."); } } function zoomToLocation(location) { pt = esri.geometry.geographicToWebMercator(new Point(location.coords.longitude, location.coords.latitude)); addGraphic(pt); map.centerAndZoom(pt, 17); } function showLocation(location) { pt = esri.geometry.geographicToWebMercator(new Point(location.coords.longitude, location.coords.latitude)); if (!graphic) { addGraphic(pt); } else { //move the graphic if it already exists graphic.setGeometry(pt); } map.centerAt(pt); } function locationError(error) { //error occurred so stop watchPosition if (navigator.geolocation) { navigator.geolocation.clearWatch(watchId); } switch (error.code) { case error.PERMISSION_DENIED: alert("Location not provided"); break; case error.POSITION_UNAVAILABLE: alert("Current location not available"); break; case error.TIMEOUT: alert("Timeout"); break; default: alert("unknown error"); break; } } // Add a pulsating graphic to the map function addGraphic(pt) { var symbol = new SimpleMarkerSymbol(SimpleMarkerSymbol.STYLE_CIRCLE, 12, new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([210, 105, 30, 0.5]), 8), new Color([210, 105, 30, 0.9])); graphic = new Graphic(pt, symbol); map.graphics.add(graphic); } function loadCompass() { compassHousing = dom.byId("compassHousing"); // assign the compass housing dimensions compassHousing.style.height = compassHousing.style.width = COMPASS_SIZE + "px"; // return the absolute position of the compass housing containerX = domGeom.position(compassHousing).x; containerY = domGeom.position(compassHousing).y; currentHeading = 0; needleAngle = 0; if (!buildCompassFace()) { return; } drawCompassFace(); drawCompassNeedle(); hasWebkit(); } // Creates the diameter of the compass face // Creates the radius function buildCompassFace() { // compass housing diameter and radius compassFaceDiameter = COMPASS_SIZE; compassFaceRadius = compassFaceDiameter / 2; // needle length needleLength = compassFaceDiameter; // needle width needleWidth = needleLength / 10; // tick marks compassRing = compassFaceDiameter / 50; return true; } var compassFaceContext; // Draw the coppass face, text labels and font, and tick marks function drawCompassFace() { var compassFaceCanvas = dom.byId("compassFace"); compassFaceCanvas.width = compassFaceCanvas.height = compassFaceDiameter; compassFaceContext = compassFaceCanvas.getContext("2d"); compassFaceContext.clearRect(0, 0, compassFaceCanvas.width, compassFaceCanvas.height); // draw the tick marks and center the compass ring var xOffset, yOffset; xOffset = yOffset = compassFaceCanvas.width / 2; for (var i = 0; i < 360; ++i) { var x = (compassFaceRadius * Math.cos(degToRad(i))) + xOffset; var y = (compassFaceRadius * Math.sin(degToRad(i))) + yOffset; var x2 = ((compassFaceRadius - compassRing) * Math.cos(degToRad(i))) + xOffset; var y2 = ((compassFaceRadius - compassRing) * Math.sin(degToRad(i))) + yOffset; compassFaceContext.beginPath(); compassFaceContext.moveTo(x, y); compassFaceContext.lineTo(x2, y2); compassFaceContext.closePath(); compassFaceContext.stroke(); i = i + 4; } // The measureText method returns an object, with one attribute: width. // The width attribute returns the width of the text, in pixels. compassFaceContext.font = "10px Arial"; compassFaceContext.textAlign = "center"; var metrics = compassFaceContext.measureText('N'); compassFaceContext.fillText('N', compassFaceRadius, 15); compassFaceContext.fillText('S', compassFaceRadius, compassFaceDiameter - 10); compassFaceContext.fillText('E', (compassFaceRadius + (compassFaceRadius - metrics.width)), compassFaceRadius); compassFaceContext.fillText('W', 10, compassFaceRadius); } // Draw the compass needle function drawCompassNeedle() { var compassNeedle = dom.byId("compassNeedle"); compassNeedle.width = compassNeedle.height = compassFaceDiameter; compassNeedle.style.left = Math.floor(compassFaceContext.width / 2) + "px"; compassNeedle.style.top = Math.floor(compassFaceContext.height / 2) + "px"; compassNeedleContext = compassNeedle.getContext("2d"); compassNeedleContext.translate(compassFaceRadius, compassFaceRadius); compassNeedleContext.clearRect((compassNeedleContext.canvas.width / 2) * -1, (compassNeedleContext.canvas.height / 2) * -1, compassNeedleContext.canvas.width, compassNeedleContext.canvas.height); // The first step to create a path is calling the beginPath method. Internally, paths are stored as a list of sub-paths // (lines, arcs, etc) which together form a shape. Every time this method is called, the list is reset and we can start // drawing new shapes. // SOUTH compassNeedleContext.beginPath(); compassNeedleContext.lineWidth = 1; compassNeedleContext.moveTo(0, 5); compassNeedleContext.lineTo(0, compassFaceRadius); compassNeedleContext.stroke(); // circle around label compassNeedleContext.beginPath(); compassNeedleContext.arc(0, compassFaceRadius - 15, 8, 0, 2 * Math.PI, false); compassNeedleContext.fillStyle = "#FFF"; compassNeedleContext.fill(); compassNeedleContext.lineWidth = 1; compassNeedleContext.strokeStyle = "black"; compassNeedleContext.stroke(); // S compassNeedleContext.beginPath(); compassNeedleContext.moveTo(0, 0); compassNeedleContext.font = "normal 10px Verdana"; compassNeedleContext.fillStyle = "#000"; compassNeedleContext.textAlign = "center"; compassNeedleContext.fillText("S", 0, compassFaceRadius - 10); // needle compassNeedleContext.beginPath(); compassNeedleContext.fillStyle = "#000"; compassNeedleContext.moveTo(0, 0); compassNeedleContext.lineTo(0, needleLength / 4); compassNeedleContext.lineTo((needleWidth / 4) * -1, 0); compassNeedleContext.fill(); compassNeedleContext.beginPath(); compassNeedleContext.fillStyle = "#000"; compassNeedleContext.moveTo(0, 0); compassNeedleContext.lineTo(0, needleLength / 4); compassNeedleContext.lineTo(needleWidth / 4, 0); compassNeedleContext.fill(); // NORTH compassNeedleContext.beginPath(); compassNeedleContext.lineWidth = 1; compassNeedleContext.moveTo(0, 0); compassNeedleContext.lineTo(0, - compassFaceRadius); compassNeedleContext.stroke(); // circle compassNeedleContext.beginPath(); compassNeedleContext.arc(0, - (compassFaceRadius - 16), 8, 0, 2 * Math.PI, false); compassNeedleContext.fillStyle = "#FFF"; compassNeedleContext.fill(); compassNeedleContext.lineWidth = 1; compassNeedleContext.strokeStyle = "black"; compassNeedleContext.stroke(); // N compassNeedleContext.beginPath(); compassNeedleContext.moveTo(0, 0); compassNeedleContext.font = "normal 10px Verdana"; compassNeedleContext.fillStyle = "#000"; compassNeedleContext.textAlign = "center"; compassNeedleContext.fillText("N", 0, - (compassFaceRadius - 20)); // needle compassNeedleContext.beginPath(); compassNeedleContext.fillStyle = "#000"; compassNeedleContext.moveTo(0, 0); compassNeedleContext.lineTo(0, (needleLength / 4) * -1); compassNeedleContext.lineTo((needleWidth / 4) * -1, 0); compassNeedleContext.fill(); compassNeedleContext.beginPath(); compassNeedleContext.fillStyle = "#000"; compassNeedleContext.moveTo(0, 0); compassNeedleContext.lineTo(0, (needleLength / 4) * -1); compassNeedleContext.lineTo(needleWidth / 4, 0); compassNeedleContext.fill(); // center pin color compassNeedleContext.beginPath(); compassNeedleContext.arc(0, 0, 10, 0, 2 * Math.PI, false); compassNeedleContext.fillStyle = "rgb(255,255,255)"; compassNeedleContext.fill(); compassNeedleContext.lineWidth = 1; compassNeedleContext.strokeStyle = "black"; compassNeedleContext.stroke(); compassNeedleContext.beginPath(); compassNeedleContext.moveTo(0, 0); compassNeedleContext.arc(0, 0, (needleWidth / 4), 0, degToRad(360), false); compassNeedleContext.fillStyle = "#000"; compassNeedleContext.fill(); } var orientationHandle; function orientationChangeHandler() { // An event handler for device orientation events sent to the window. orientationHandle = on(window, "deviceorientation", onDeviceOrientationChange); // The setInterval() method calls rotateNeedle at specified intervals (in milliseconds). renderingInterval = setInterval(rotateNeedle, 100); } var compassTestHandle; function hasWebkit() { if (has("ff") || has("ie") || has("opera")) { hasCompass = false; orientationChangeHandler(); alert("Your browser does not support WebKit."); } else if (window.DeviceOrientationEvent) { compassTestHandle = on(window, "deviceorientation", hasGyroscope); } else { hasCompass = false; orientationChangeHandler(); } } // Test if the device has a gyroscope. // Instances of the DeviceOrientationEvent class are fired only when the device has a gyroscope and while the user is changing the orientation. function hasGyroscope(event) { dojo.disconnect(compassTestHandle); if (event.webkitCompassHeading !== undefined || event.alpha != null) { hasCompass = true; } else { hasCompass = false; } orientationChangeHandler(); } // Rotate the needle based on the device's current heading function rotateNeedle() { var multiplier = Math.floor(needleAngle / 360); var adjustedNeedleAngle = needleAngle - (360 * multiplier); var delta = currentHeading - adjustedNeedleAngle; if (Math.abs(delta) > 180) { if (delta < 0) { delta += 360; } else { delta -= 360; } } delta /= 5; needleAngle = needleAngle + delta; var updatedAngle = needleAngle - window.orientation; // rotate the needle dom.byId("compassNeedle").style.webkitTransform = "rotate(" + updatedAngle + "deg)"; } function onDeviceOrientationChange(event) { var accuracy; if (event.webkitCompassHeading !== undefined) { // Direction values are measured in degrees starting at due north and continuing clockwise around the compass. // Thus, north is 0 degrees, east is 90 degrees, south is 180 degrees, and so on. A negative value indicates an invalid direction. currentHeading = (360 - event.webkitCompassHeading); accuracy = event.webkitCompassAccuracy; } else if (event.alpha != null) { // alpha returns the rotation of the device around the Z axis; that is, the number of degrees by which the device is being twisted // around the center of the screen // (support for android) currentHeading = (270 - event.alpha) * -1; accuracy = event.webkitCompassAccuracy; } if (accuracy < 11) { compassNeedleContext.fillStyle = "rgba(0, 205, 0, 0.9)"; } else if (accuracy >= 15 && accuracy < 25) { compassNeedleContext.fillStyle = "rgba(255, 255, 0, 0.9)"; } else if (accuracy > 24) { compassNeedleContext.fillStyle = "rgba(255, 0, 0, 0.9)"; } compassNeedleContext.fill(); if (renderingInterval == -1) { rotateNeedle(); } } // Convert degrees to radians function degToRad(deg) { return (deg * Math.PI) / 180; } // Handle portrait and landscape mode orientation changes function orientationChanged() { if (map) { map.reposition(); map.resize(); } } }); </script> </head> <body> <article id="compassHousing"> <canvas id="compassFace"></canvas> <canvas id="compassNeedle"></canvas> </article> <div id="map"></div> </body> </html>