Modern web applications can do far more than display content. Through geolocation and hardware APIs, web apps can access device location, camera, microphone, accelerometer, gyroscope, battery status, and dozens of other hardware features. This capability transforms what’s possible on the web, enabling location-based services, augmented reality experiences, fitness tracking, IoT integrations, and much more.
Why This Matters:
- 78% of users expect personalized location-based experiences
- Web apps can now access 50+ device APIs for hardware capabilities
- PWAs with hardware access see 3-5x higher engagement rates
- Cross-platform development saves 40-60% development time vs native apps
However, with great power comes great responsibility. These APIs access sensitive device capabilities and personal information. Implementing them correctly requires understanding not just the technical implementation, but also security, privacy, performance, and user experience considerations.
What You’ll Learn:
- Complete implementation of geolocation and 15+ hardware APIs
- Permission handling and error management strategies
- Security and privacy best practices with GDPR compliance
- Performance optimization for battery and network efficiency
- Testing strategies for device APIs
- Real-world use cases and production patterns
- Accessibility considerations for hardware features
- Cross-browser compatibility and polyfills
Prerequisites:
- JavaScript ES6+ knowledge
- Understanding of async/await and Promises
- Basic knowledge of web security (HTTPS, CORS)
- Familiarity with browser DevTools
Understanding Geolocation and Hardware APIs
What Are These APIs?
Geolocation API: Provides access to a user’s geographic location (latitude, longitude, altitude, accuracy).
Hardware APIs: Provide access to device hardware and sensors:
| API | Capability | Use Cases | Browser Support |
|---|---|---|---|
| MediaStream | Camera, microphone | Video calls, recording, scanning | โ Excellent |
| Geolocation | GPS location | Maps, local search, tracking | โ Excellent |
| DeviceMotion | Accelerometer, gyroscope | Games, fitness, shake detection | โ Excellent |
| DeviceOrientation | Compass, tilt | AR, 360ยฐ viewers, compass | โ Excellent |
| Battery Status | Battery level, charging | Power optimization | โ ๏ธ Limited |
| Vibration | Haptic feedback | Games, notifications | โ Good |
| Screen Wake Lock | Prevent screen sleep | Video, recipes, presentations | โ Growing |
| Ambient Light | Light sensor | Auto brightness | โ ๏ธ Experimental |
| Web Bluetooth | Bluetooth devices | IoT, wearables, peripherals | โ ๏ธ Limited |
| Web NFC | NFC tags | Payments, access control | โ ๏ธ Experimental |
| Web USB | USB devices | Hardware development | โ ๏ธ Limited |
| Gamepad | Game controllers | Gaming | โ Good |
| Clipboard | Copy/paste | Productivity apps | โ Excellent |
| Pointer Lock | Mouse capture | Gaming, 3D modeling | โ Good |
| Fullscreen | Fullscreen mode | Presentations, games | โ Excellent |
Why They Matter
These APIs enable:
- Location-based services: Maps, navigation, local search, location check-ins
- Augmented reality: Virtual try-ons, AR games, spatial computing
- Accessibility: Voice control, gesture recognition, adaptive interfaces
- Fitness and health: Step counting, activity tracking, workout monitoring
- Immersive experiences: VR, motion-controlled games, interactive installations
- Convenience: Auto-fill location, context-aware features, personalization
The Geolocation API
The Geolocation API is one of the most commonly used hardware APIs. It provides access to a user’s location with their explicit permission.
Basic Implementation
// Check if Geolocation API is supported
if ('geolocation' in navigator) {
// Get current position
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude, accuracy } = position.coords;
console.log(`Location: ${latitude}, ${longitude}`);
console.log(`Accuracy: ${accuracy} meters`);
},
(error) => {
console.error('Geolocation error:', error.message);
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
} else {
console.log('Geolocation not supported');
}
Position Object Properties
The position object contains:
position.coords = {
latitude: 40.7128, // Degrees
longitude: -74.0060, // Degrees
accuracy: 50, // Meters
altitude: 10, // Meters (may be null)
altitudeAccuracy: 5, // Meters (may be null)
heading: 90, // Degrees (0-360, null if stationary)
speed: 5.5 // Meters per second (null if stationary)
};
position.timestamp = 1234567890; // Milliseconds since epoch
Watching Location Changes
For continuous location tracking:
class LocationTracker {
constructor() {
this.watchId = null;
this.locations = [];
}
startTracking() {
if (!('geolocation' in navigator)) {
console.error('Geolocation not supported');
return;
}
this.watchId = navigator.geolocation.watchPosition(
(position) => this.handlePositionUpdate(position),
(error) => this.handleError(error),
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
}
handlePositionUpdate(position) {
const { latitude, longitude, accuracy, timestamp } = position.coords;
const location = {
latitude,
longitude,
accuracy,
timestamp,
distance: this.calculateDistance()
};
this.locations.push(location);
this.updateUI(location);
}
calculateDistance() {
if (this.locations.length < 2) return 0;
const prev = this.locations[this.locations.length - 2];
const curr = this.locations[this.locations.length - 1];
return this.haversineDistance(
prev.latitude, prev.longitude,
curr.latitude, curr.longitude
);
}
haversineDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Earth's radius in km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
handleError(error) {
switch (error.code) {
case error.PERMISSION_DENIED:
console.error('User denied geolocation permission');
break;
case error.POSITION_UNAVAILABLE:
console.error('Position information unavailable');
break;
case error.TIMEOUT:
console.error('Geolocation request timed out');
break;
}
}
stopTracking() {
if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId);
this.watchId = null;
}
}
updateUI(location) {
// Update map, display location, etc.
console.log(`Current location: ${location.latitude}, ${location.longitude}`);
}
}
// Usage
const tracker = new LocationTracker();
tracker.startTracking();
// Stop tracking when done
// tracker.stopTracking();
Advanced Geolocation Patterns
Geofencing
Detect when users enter or leave specific areas:
class Geofence {
constructor(center, radius) {
this.center = center; // { lat, lng }
this.radius = radius; // meters
this.listeners = new Set();
}
isInside(point) {
const distance = this.calculateDistance(
this.center.lat, this.center.lng,
point.lat, point.lng
);
return distance <= this.radius;
}
calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371000; // Earth's radius in meters
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
onEnter(callback) {
this.listeners.add({ type: 'enter', callback });
}
onExit(callback) {
this.listeners.add({ type: 'exit', callback });
}
}
class GeofenceManager {
constructor() {
this.geofences = new Map();
this.watchId = null;
this.lastStatus = new Map();
}
addGeofence(id, center, radius) {
const geofence = new Geofence(center, radius);
this.geofences.set(id, geofence);
return geofence;
}
startMonitoring() {
this.watchId = navigator.geolocation.watchPosition(
(position) => this.checkGeofences(position),
(error) => console.error('Geofence error:', error),
{ enableHighAccuracy: true, maximumAge: 0 }
);
}
checkGeofences(position) {
const point = {
lat: position.coords.latitude,
lng: position.coords.longitude
};
this.geofences.forEach((geofence, id) => {
const isInside = geofence.isInside(point);
const wasInside = this.lastStatus.get(id);
if (isInside && !wasInside) {
// Entered geofence
geofence.listeners.forEach(listener => {
if (listener.type === 'enter') {
listener.callback(id, point);
}
});
} else if (!isInside && wasInside) {
// Exited geofence
geofence.listeners.forEach(listener => {
if (listener.type === 'exit') {
listener.callback(id, point);
}
});
}
this.lastStatus.set(id, isInside);
});
}
stopMonitoring() {
if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId);
}
}
}
// Usage
const manager = new GeofenceManager();
// Add geofence around office
const officeGeofence = manager.addGeofence(
'office',
{ lat: 40.7128, lng: -74.0060 },
100 // 100 meters radius
);
officeGeofence.onEnter((id, location) => {
console.log('Entered office area');
// Auto check-in, enable office mode, etc.
});
officeGeofence.onExit((id, location) => {
console.log('Left office area');
// Auto check-out, disable office mode, etc.
});
manager.startMonitoring();
Route Tracking with Speed and Distance
class RouteTracker {
constructor() {
this.route = [];
this.totalDistance = 0;
this.startTime = null;
this.watchId = null;
}
start() {
this.startTime = Date.now();
this.route = [];
this.totalDistance = 0;
this.watchId = navigator.geolocation.watchPosition(
(position) => this.addPoint(position),
(error) => console.error('Route tracking error:', error),
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
}
addPoint(position) {
const point = {
lat: position.coords.latitude,
lng: position.coords.longitude,
altitude: position.coords.altitude,
accuracy: position.coords.accuracy,
speed: position.coords.speed,
timestamp: position.timestamp
};
if (this.route.length > 0) {
const lastPoint = this.route[this.route.length - 1];
const distance = this.calculateDistance(
lastPoint.lat, lastPoint.lng,
point.lat, point.lng
);
// Filter out noise (points too close)
if (distance > 5) { // 5 meters threshold
this.totalDistance += distance;
this.route.push(point);
this.updateStats();
}
} else {
this.route.push(point);
}
}
calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371000; // meters
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
updateStats() {
const duration = (Date.now() - this.startTime) / 1000; // seconds
const avgSpeed = this.totalDistance / duration; // m/s
const stats = {
distance: this.totalDistance,
duration: duration,
avgSpeed: avgSpeed * 3.6, // km/h
points: this.route.length
};
this.onStatsUpdate(stats);
}
onStatsUpdate(stats) {
console.log(`Distance: ${(stats.distance / 1000).toFixed(2)} km`);
console.log(`Duration: ${Math.floor(stats.duration / 60)} min`);
console.log(`Avg Speed: ${stats.avgSpeed.toFixed(1)} km/h`);
}
stop() {
if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId);
}
return this.getRouteData();
}
getRouteData() {
return {
route: this.route,
totalDistance: this.totalDistance,
duration: (Date.now() - this.startTime) / 1000,
startTime: this.startTime,
endTime: Date.now()
};
}
exportGPX() {
// Export route as GPX format
let gpx = '<?xml version="1.0" encoding="UTF-8"?>\n';
gpx += '<gpx version="1.1" creator="RouteTracker">\n';
gpx += ' <trk>\n <trkseg>\n';
this.route.forEach(point => {
gpx += ` <trkpt lat="${point.lat}" lon="${point.lng}">\n`;
if (point.altitude) {
gpx += ` <ele>${point.altitude}</ele>\n`;
}
gpx += ` <time>${new Date(point.timestamp).toISOString()}</time>\n`;
gpx += ' </trkpt>\n';
});
gpx += ' </trkseg>\n </trk>\n</gpx>';
return gpx;
}
}
// Usage
const routeTracker = new RouteTracker();
routeTracker.start();
// Later...
const routeData = routeTracker.stop();
const gpxData = routeTracker.exportGPX();
Hardware APIs
Camera and Microphone (MediaStream API)
Access camera and microphone for video/audio capture:
class MediaCapture {
constructor() {
this.stream = null;
this.mediaRecorder = null;
}
async requestCameraAccess() {
try {
this.stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
},
audio: true
});
// Display video stream
const video = document.getElementById('video');
video.srcObject = this.stream;
return this.stream;
} catch (error) {
console.error('Camera access denied:', error);
throw error;
}
}
async requestMicrophoneOnly() {
try {
this.stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
return this.stream;
} catch (error) {
console.error('Microphone access denied:', error);
throw error;
}
}
startRecording() {
if (!this.stream) {
console.error('No media stream available');
return;
}
this.mediaRecorder = new MediaRecorder(this.stream);
const chunks = [];
this.mediaRecorder.ondataavailable = (event) => {
chunks.push(event.data);
};
this.mediaRecorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
this.handleRecordedMedia(blob);
};
this.mediaRecorder.start();
}
stopRecording() {
if (this.mediaRecorder) {
this.mediaRecorder.stop();
}
}
handleRecordedMedia(blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `recording-${Date.now()}.webm`;
a.click();
}
stopStream() {
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
this.stream = null;
}
}
}
// Usage
const capture = new MediaCapture();
document.getElementById('camera-btn').addEventListener('click', async () => {
await capture.requestCameraAccess();
});
Motion and Orientation APIs
Access device accelerometer and gyroscope:
class MotionDetector {
constructor() {
this.isSupported = {
deviceMotion: 'DeviceMotionEvent' in window,
deviceOrientation: 'DeviceOrientationEvent' in window,
permission: 'DeviceMotionEvent' in window &&
typeof DeviceMotionEvent !== 'undefined' &&
typeof DeviceMotionEvent.requestPermission === 'function'
};
}
async requestPermission() {
if (!this.isSupported.permission) {
console.log('Permission request not needed');
return true;
}
try {
const permission = await DeviceMotionEvent.requestPermission();
return permission === 'granted';
} catch (error) {
console.error('Permission request failed:', error);
return false;
}
}
startMotionTracking() {
if (!this.isSupported.deviceMotion) {
console.error('Device Motion API not supported');
return;
}
window.addEventListener('devicemotion', (event) => {
const { x, y, z } = event.acceleration;
const { alpha, beta, gamma } = event.rotationRate;
console.log('Acceleration:', { x, y, z });
console.log('Rotation:', { alpha, beta, gamma });
this.handleMotion({ x, y, z }, { alpha, beta, gamma });
});
}
startOrientationTracking() {
if (!this.isSupported.deviceOrientation) {
console.error('Device Orientation API not supported');
return;
}
window.addEventListener('deviceorientation', (event) => {
const { alpha, beta, gamma } = event;
console.log('Orientation:', { alpha, beta, gamma });
this.handleOrientation({ alpha, beta, gamma });
});
}
handleMotion(acceleration, rotation) {
// Detect shake
const magnitude = Math.sqrt(
acceleration.x ** 2 +
acceleration.y ** 2 +
acceleration.z ** 2
);
if (magnitude > 25) {
console.log('Device shaken!');
this.onShake();
}
}
handleOrientation(orientation) {
// Use orientation for game controls, AR, etc.
const { alpha, beta, gamma } = orientation;
// Rotate element based on device orientation
const element = document.getElementById('rotating-element');
if (element) {
element.style.transform = `rotateX(${beta}deg) rotateY(${gamma}deg)`;
}
}
onShake() {
// Handle shake event
console.log('Shake detected!');
}
stopTracking() {
window.removeEventListener('devicemotion', this.handleMotion);
window.removeEventListener('deviceorientation', this.handleOrientation);
}
}
// Usage
const motion = new MotionDetector();
// Request permission (iOS 13+)
if (motion.isSupported.permission) {
document.getElementById('motion-btn').addEventListener('click', async () => {
const granted = await motion.requestPermission();
if (granted) {
motion.startMotionTracking();
motion.startOrientationTracking();
}
});
} else {
motion.startMotionTracking();
motion.startOrientationTracking();
}
Battery Status API
Monitor device battery level:
class BatteryMonitor {
constructor() {
this.battery = null;
}
async init() {
if (!('getBattery' in navigator)) {
console.log('Battery Status API not supported');
return;
}
try {
this.battery = await navigator.getBattery();
this.setupListeners();
this.updateUI();
} catch (error) {
console.error('Battery API error:', error);
}
}
setupListeners() {
this.battery.addEventListener('levelchange', () => this.updateUI());
this.battery.addEventListener('chargingchange', () => this.updateUI());
this.battery.addEventListener('chargingtimechange', () => this.updateUI());
this.battery.addEventListener('dischargingtimechange', () => this.updateUI());
}
updateUI() {
const level = Math.round(this.battery.level * 100);
const charging = this.battery.charging;
const chargingTime = this.battery.chargingTime;
const dischargingTime = this.battery.dischargingTime;
console.log(`Battery: ${level}%`);
console.log(`Charging: ${charging}`);
console.log(`Time to full: ${chargingTime}s`);
console.log(`Time to empty: ${dischargingTime}s`);
// Adjust app behavior based on battery
if (level < 20 && !charging) {
this.enableBatterySaver();
}
}
enableBatterySaver() {
console.log('Battery saver mode enabled');
// Reduce animations, disable background sync, etc.
}
}
// Usage
const battery = new BatteryMonitor();
battery.init();
Vibration API
Provide haptic feedback:
class HapticFeedback {
static isSupported() {
return 'vibrate' in navigator;
}
static vibrate(pattern) {
if (!this.isSupported()) {
console.log('Vibration API not supported');
return;
}
navigator.vibrate(pattern);
}
static pulse() {
this.vibrate(100); // 100ms vibration
}
static doubleTap() {
this.vibrate([50, 50, 50]); // 50ms on, 50ms off, 50ms on
}
static success() {
this.vibrate([100, 50, 100]); // Success pattern
}
static error() {
this.vibrate([200, 100, 200, 100, 200]); // Error pattern
}
static stop() {
this.vibrate(0); // Stop vibration
}
}
// Usage
document.getElementById('button').addEventListener('click', () => {
HapticFeedback.pulse();
});
document.getElementById('success-btn').addEventListener('click', () => {
HapticFeedback.success();
});
Screen Wake Lock API
Prevent screen from sleeping during important activities:
class ScreenWakeLock {
constructor() {
this.wakeLock = null;
}
static isSupported() {
return 'wakeLock' in navigator;
}
async request() {
if (!ScreenWakeLock.isSupported()) {
console.log('Wake Lock API not supported');
return false;
}
try {
this.wakeLock = await navigator.wakeLock.request('screen');
console.log('Wake Lock acquired');
// Re-acquire wake lock when page becomes visible
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible' && this.wakeLock !== null) {
await this.request();
}
});
// Listen for wake lock release
this.wakeLock.addEventListener('release', () => {
console.log('Wake Lock released');
});
return true;
} catch (error) {
console.error('Wake Lock error:', error);
return false;
}
}
async release() {
if (this.wakeLock !== null) {
await this.wakeLock.release();
this.wakeLock = null;
}
}
}
// Usage
const wakeLock = new ScreenWakeLock();
// Request wake lock during video playback
document.getElementById('video').addEventListener('play', async () => {
await wakeLock.request();
});
document.getElementById('video').addEventListener('pause', async () => {
await wakeLock.release();
});
Web Bluetooth API
Connect to Bluetooth Low Energy devices:
class BluetoothManager {
constructor() {
this.device = null;
this.server = null;
this.characteristic = null;
}
static isSupported() {
return 'bluetooth' in navigator;
}
async scan(serviceUUID) {
if (!BluetoothManager.isSupported()) {
throw new Error('Web Bluetooth not supported');
}
try {
// Request device
this.device = await navigator.bluetooth.requestDevice({
filters: [{ services: [serviceUUID] }]
});
console.log('Device found:', this.device.name);
// Add disconnect listener
this.device.addEventListener('gattserverdisconnected', () => {
console.log('Device disconnected');
this.onDisconnect();
});
return this.device;
} catch (error) {
console.error('Bluetooth scan error:', error);
throw error;
}
}
async connect() {
if (!this.device) {
throw new Error('No device selected');
}
try {
// Connect to GATT server
this.server = await this.device.gatt.connect();
console.log('Connected to GATT server');
return this.server;
} catch (error) {
console.error('Bluetooth connect error:', error);
throw error;
}
}
async getCharacteristic(serviceUUID, characteristicUUID) {
if (!this.server) {
throw new Error('Not connected');
}
try {
const service = await this.server.getPrimaryService(serviceUUID);
this.characteristic = await service.getCharacteristic(characteristicUUID);
return this.characteristic;
} catch (error) {
console.error('Get characteristic error:', error);
throw error;
}
}
async readValue() {
if (!this.characteristic) {
throw new Error('No characteristic selected');
}
const value = await this.characteristic.readValue();
return value;
}
async writeValue(data) {
if (!this.characteristic) {
throw new Error('No characteristic selected');
}
await this.characteristic.writeValue(data);
}
async startNotifications(callback) {
if (!this.characteristic) {
throw new Error('No characteristic selected');
}
await this.characteristic.startNotifications();
this.characteristic.addEventListener('characteristicvaluechanged', (event) => {
callback(event.target.value);
});
}
async disconnect() {
if (this.device && this.device.gatt.connected) {
this.device.gatt.disconnect();
}
}
onDisconnect() {
console.log('Bluetooth device disconnected');
// Handle disconnection
}
}
// Usage: Connect to heart rate monitor
const bluetooth = new BluetoothManager();
document.getElementById('connect-btn').addEventListener('click', async () => {
try {
const HEART_RATE_SERVICE = 'heart_rate';
const HEART_RATE_MEASUREMENT = 'heart_rate_measurement';
await bluetooth.scan(HEART_RATE_SERVICE);
await bluetooth.connect();
await bluetooth.getCharacteristic(HEART_RATE_SERVICE, HEART_RATE_MEASUREMENT);
// Start receiving heart rate data
await bluetooth.startNotifications((value) => {
const heartRate = value.getUint8(1);
console.log('Heart Rate:', heartRate, 'bpm');
document.getElementById('heart-rate').textContent = heartRate;
});
} catch (error) {
console.error('Bluetooth error:', error);
}
});
Web NFC API
Read and write NFC tags:
class NFCManager {
constructor() {
this.reader = null;
this.writer = null;
}
static async isSupported() {
if (!('NDEFReader' in window)) {
return false;
}
try {
const permission = await navigator.permissions.query({ name: 'nfc' });
return permission.state === 'granted' || permission.state === 'prompt';
} catch (error) {
return false;
}
}
async startReading() {
if (!await NFCManager.isSupported()) {
throw new Error('Web NFC not supported');
}
try {
this.reader = new NDEFReader();
await this.reader.scan();
console.log('NFC scan started');
this.reader.addEventListener('reading', ({ message, serialNumber }) => {
console.log('NFC tag detected:', serialNumber);
this.handleNFCMessage(message);
});
this.reader.addEventListener('readingerror', (error) => {
console.error('NFC read error:', error);
});
} catch (error) {
console.error('NFC scan error:', error);
throw error;
}
}
handleNFCMessage(message) {
for (const record of message.records) {
console.log('Record type:', record.recordType);
console.log('MIME type:', record.mediaType);
if (record.recordType === 'text') {
const textDecoder = new TextDecoder(record.encoding);
const text = textDecoder.decode(record.data);
console.log('Text:', text);
this.onTextRead(text);
} else if (record.recordType === 'url') {
const textDecoder = new TextDecoder();
const url = textDecoder.decode(record.data);
console.log('URL:', url);
this.onURLRead(url);
}
}
}
async write(data) {
if (!await NFCManager.isSupported()) {
throw new Error('Web NFC not supported');
}
try {
this.writer = new NDEFReader();
await this.writer.write({
records: [{ recordType: 'text', data: data }]
});
console.log('NFC write successful');
} catch (error) {
console.error('NFC write error:', error);
throw error;
}
}
async writeURL(url) {
if (!await NFCManager.isSupported()) {
throw new Error('Web NFC not supported');
}
try {
this.writer = new NDEFReader();
await this.writer.write({
records: [{ recordType: 'url', data: url }]
});
console.log('NFC URL written');
} catch (error) {
console.error('NFC write error:', error);
throw error;
}
}
onTextRead(text) {
console.log('Text from NFC tag:', text);
}
onURLRead(url) {
console.log('URL from NFC tag:', url);
}
}
// Usage
const nfc = new NFCManager();
// Start reading NFC tags
document.getElementById('scan-nfc').addEventListener('click', async () => {
try {
await nfc.startReading();
console.log('Hold NFC tag near device...');
} catch (error) {
console.error('NFC error:', error);
}
});
// Write to NFC tag
document.getElementById('write-nfc').addEventListener('click', async () => {
try {
await nfc.writeURL('https://example.com');
console.log('Touch tag to write...');
} catch (error) {
console.error('NFC write error:', error);
}
});
Gamepad API
Access game controllers:
class GamepadManager {
constructor() {
this.gamepads = {};
this.animationFrame = null;
}
init() {
window.addEventListener('gamepadconnected', (e) => {
console.log('Gamepad connected:', e.gamepad.id);
this.gamepads[e.gamepad.index] = e.gamepad;
this.startPolling();
});
window.addEventListener('gamepaddisconnected', (e) => {
console.log('Gamepad disconnected:', e.gamepad.id);
delete this.gamepads[e.gamepad.index];
if (Object.keys(this.gamepads).length === 0) {
this.stopPolling();
}
});
}
startPolling() {
if (this.animationFrame) return;
const poll = () => {
this.updateGamepads();
this.animationFrame = requestAnimationFrame(poll);
};
poll();
}
stopPolling() {
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
}
updateGamepads() {
const gamepads = navigator.getGamepads();
for (const gamepad of gamepads) {
if (gamepad) {
this.handleGamepadInput(gamepad);
}
}
}
handleGamepadInput(gamepad) {
// Handle buttons
gamepad.buttons.forEach((button, index) => {
if (button.pressed) {
console.log(`Button ${index} pressed`);
this.onButtonPress(index, button.value);
}
});
// Handle axes (joysticks)
const [leftX, leftY, rightX, rightY] = gamepad.axes;
// Dead zone to prevent drift
const deadZone = 0.1;
if (Math.abs(leftX) > deadZone || Math.abs(leftY) > deadZone) {
this.onLeftStick(leftX, leftY);
}
if (Math.abs(rightX) > deadZone || Math.abs(rightY) > deadZone) {
this.onRightStick(rightX, rightY);
}
}
onButtonPress(button, value) {
console.log(`Button ${button} pressed with value ${value}`);
}
onLeftStick(x, y) {
console.log(`Left stick: ${x.toFixed(2)}, ${y.toFixed(2)}`);
}
onRightStick(x, y) {
console.log(`Right stick: ${x.toFixed(2)}, ${y.toFixed(2)}`);
}
}
// Usage
const gamepadManager = new GamepadManager();
gamepadManager.init();
Performance Optimization
Battery-Aware Location Tracking
Adjust tracking frequency based on battery level:
class BatteryAwareTracker {
constructor() {
this.battery = null;
this.watchId = null;
this.updateInterval = 5000; // Default 5 seconds
}
async init() {
if ('getBattery' in navigator) {
this.battery = await navigator.getBattery();
this.adjustTrackingInterval();
// Update interval when battery changes
this.battery.addEventListener('levelchange', () => {
this.adjustTrackingInterval();
});
this.battery.addEventListener('chargingchange', () => {
this.adjustTrackingInterval();
});
}
this.startTracking();
}
adjustTrackingInterval() {
const level = this.battery.level;
const charging = this.battery.charging;
if (charging) {
this.updateInterval = 1000; // 1 second when charging
} else if (level > 0.5) {
this.updateInterval = 5000; // 5 seconds when >50%
} else if (level > 0.2) {
this.updateInterval = 15000; // 15 seconds when 20-50%
} else {
this.updateInterval = 30000; // 30 seconds when <20%
}
console.log(`Tracking interval: ${this.updateInterval}ms`);
// Restart tracking with new interval
if (this.watchId !== null) {
this.stopTracking();
this.startTracking();
}
}
startTracking() {
this.watchId = navigator.geolocation.watchPosition(
(position) => this.handlePosition(position),
(error) => console.error(error),
{
enableHighAccuracy: this.battery?.level > 0.5,
timeout: this.updateInterval,
maximumAge: this.updateInterval / 2
}
);
}
stopTracking() {
if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId);
this.watchId = null;
}
}
handlePosition(position) {
console.log('Position:', position.coords);
}
}
Throttling and Debouncing Sensor Data
class OptimizedSensorReader {
constructor() {
this.throttleDelay = 100; // ms
this.lastUpdate = 0;
this.debounceTimeout = null;
}
// Throttle: Execute at most once per time period
throttle(callback, delay = this.throttleDelay) {
return (...args) => {
const now = Date.now();
if (now - this.lastUpdate >= delay) {
this.lastUpdate = now;
callback.apply(this, args);
}
};
}
// Debounce: Execute after activity stops
debounce(callback, delay = 300) {
return (...args) => {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(() => {
callback.apply(this, args);
}, delay);
};
}
startOptimizedMotionTracking() {
const handleMotion = this.throttle((event) => {
const { x, y, z } = event.acceleration;
console.log('Motion (throttled):', { x, y, z });
}, 100);
window.addEventListener('devicemotion', handleMotion);
}
startOptimizedOrientationTracking() {
const handleOrientation = this.debounce((event) => {
const { alpha, beta, gamma } = event;
console.log('Orientation (debounced):', { alpha, beta, gamma });
}, 200);
window.addEventListener('deviceorientation', handleOrientation);
}
}
Efficient Media Stream Management
class OptimizedMediaCapture {
constructor() {
this.stream = null;
this.tracks = [];
}
async startCamera(lowPower = false) {
const constraints = {
video: lowPower ? {
width: { ideal: 640 },
height: { ideal: 480 },
frameRate: { ideal: 15 }
} : {
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 30 }
}
};
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
return this.stream;
}
async applyConstraints(constraints) {
const videoTrack = this.stream.getVideoTracks()[0];
await videoTrack.applyConstraints(constraints);
}
pauseTrack(kind = 'video') {
const tracks = this.stream?.getTracks();
tracks?.forEach(track => {
if (track.kind === kind) {
track.enabled = false;
}
});
}
resumeTrack(kind = 'video') {
const tracks = this.stream?.getTracks();
tracks?.forEach(track => {
if (track.kind === kind) {
track.enabled = true;
}
});
}
stopAllTracks() {
this.stream?.getTracks().forEach(track => track.stop());
this.stream = null;
}
}
Caching and Offline Support
class OfflineLocationCache {
constructor() {
this.dbName = 'LocationCache';
this.storeName = 'locations';
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
});
}
async saveLocation(position) {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const location = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
timestamp: position.timestamp,
synced: navigator.onLine
};
await store.add(location);
if (navigator.onLine) {
await this.syncLocations();
}
}
async syncLocations() {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const index = store.index('timestamp');
const unsyncedLocations = [];
const request = index.openCursor();
request.onsuccess = async (event) => {
const cursor = event.target.result;
if (cursor) {
if (!cursor.value.synced) {
unsyncedLocations.push(cursor.value);
}
cursor.continue();
} else {
// Sync all unsynced locations
if (unsyncedLocations.length > 0) {
await this.uploadLocations(unsyncedLocations);
}
}
};
}
async uploadLocations(locations) {
try {
await fetch('/api/locations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(locations)
});
// Mark as synced
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
locations.forEach(location => {
location.synced = true;
store.put(location);
});
} catch (error) {
console.error('Sync failed:', error);
}
}
}
Security and Privacy Best Practices
Permission Handling
Always request permissions explicitly and handle denials gracefully:
class PermissionManager {
static async checkPermission(name) {
if (!('permissions' in navigator)) {
console.log('Permissions API not supported');
return null;
}
try {
const result = await navigator.permissions.query({ name });
return result.state; // 'granted', 'denied', or 'prompt'
} catch (error) {
console.error('Permission check failed:', error);
return null;
}
}
static async requestGeolocation() {
const state = await this.checkPermission('geolocation');
if (state === 'granted') {
console.log('Geolocation already granted');
return true;
}
if (state === 'denied') {
console.log('Geolocation denied');
this.showPermissionDeniedMessage();
return false;
}
// Request permission
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(
() => resolve(true),
() => {
this.showPermissionDeniedMessage();
resolve(false);
}
);
});
}
static showPermissionDeniedMessage() {
const message = document.createElement('div');
message.className = 'permission-denied';
message.innerHTML = `
<p>This feature requires permission.
<a href="#" onclick="location.reload()">Enable in settings</a></p>
`;
document.body.appendChild(message);
}
}
HTTPS Requirement
Most hardware APIs require HTTPS:
function checkSecureContext() {
if (!window.isSecureContext) {
console.warn('Hardware APIs require HTTPS');
return false;
}
return true;
}
// Check before using APIs
if (checkSecureContext()) {
// Safe to use hardware APIs
}
Data Privacy
Never store sensitive location or hardware data without encryption:
class SecureDataStorage {
static async storeLocationData(location) {
// Encrypt before storing
const encrypted = await this.encrypt(JSON.stringify(location));
// Store in IndexedDB with encryption
const db = await this.openDatabase();
const transaction = db.transaction(['locations'], 'readwrite');
const store = transaction.objectStore('locations');
store.add({
data: encrypted,
timestamp: Date.now()
});
}
static async encrypt(data) {
// Use Web Crypto API for encryption
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
// Generate key
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
// Encrypt
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
dataBuffer
);
return { encrypted, iv, key };
}
}
GDPR Compliance
Implement proper consent and data management:
class GDPRCompliance {
constructor() {
this.consentGiven = false;
this.dataRetentionDays = 30;
}
showConsentDialog() {
return new Promise((resolve) => {
const dialog = document.createElement('div');
dialog.className = 'consent-dialog';
dialog.innerHTML = `
<div class="consent-content">
<h2>Location Access Permission</h2>
<p>We need access to your location to:</p>
<ul>
<li>Show nearby locations</li>
<li>Provide personalized recommendations</li>
<li>Improve service quality</li>
</ul>
<p>Your data will be:</p>
<ul>
<li>Encrypted in transit and at rest</li>
<li>Stored for ${this.dataRetentionDays} days</li>
<li>Never shared with third parties without consent</li>
<li>Deletable at any time</li>
</ul>
<p>
<a href="/privacy-policy" target="_blank">Read Privacy Policy</a>
</p>
<div class="consent-buttons">
<button id="consent-accept">Accept</button>
<button id="consent-decline">Decline</button>
</div>
</div>
`;
document.body.appendChild(dialog);
document.getElementById('consent-accept').addEventListener('click', () => {
this.grantConsent();
document.body.removeChild(dialog);
resolve(true);
});
document.getElementById('consent-decline').addEventListener('click', () => {
document.body.removeChild(dialog);
resolve(false);
});
});
}
grantConsent() {
this.consentGiven = true;
localStorage.setItem('location-consent', JSON.stringify({
granted: true,
timestamp: Date.now(),
version: '1.0'
}));
}
revokeConsent() {
this.consentGiven = false;
localStorage.removeItem('location-consent');
this.deleteAllUserData();
}
async deleteAllUserData() {
// Delete from IndexedDB
const db = await this.openDatabase();
const transaction = db.transaction(['locations'], 'readwrite');
const store = transaction.objectStore('locations');
await store.clear();
// Delete from server
await fetch('/api/user/data', {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${this.getAuthToken()}` }
});
console.log('All user data deleted');
}
async cleanupOldData() {
const cutoffDate = Date.now() - (this.dataRetentionDays * 24 * 60 * 60 * 1000);
const db = await this.openDatabase();
const transaction = db.transaction(['locations'], 'readwrite');
const store = transaction.objectStore('locations');
const index = store.index('timestamp');
const range = IDBKeyRange.upperBound(cutoffDate);
const request = index.openCursor(range);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
cursor.delete();
cursor.continue();
}
};
}
hasConsent() {
const consent = localStorage.getItem('location-consent');
if (!consent) return false;
const { granted, timestamp } = JSON.parse(consent);
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
// Re-request consent after 30 days
if (timestamp < thirtyDaysAgo) {
return false;
}
return granted;
}
openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('UserData', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('locations')) {
const store = db.createObjectStore('locations', { autoIncrement: true });
store.createIndex('timestamp', 'timestamp');
}
};
});
}
getAuthToken() {
return localStorage.getItem('auth-token');
}
}
// Usage
const gdpr = new GDPRCompliance();
// Check consent before accessing location
async function requestLocation() {
if (!gdpr.hasConsent()) {
const consent = await gdpr.showConsentDialog();
if (!consent) {
console.log('Location access denied by user');
return;
}
}
// Proceed with location request
navigator.geolocation.getCurrentPosition(
(position) => console.log(position),
(error) => console.error(error)
);
}
// Allow users to delete their data
document.getElementById('delete-data').addEventListener('click', async () => {
if (confirm('Delete all your location data?')) {
await gdpr.deleteAllUserData();
alert('All data deleted');
}
});
// Cleanup old data periodically
setInterval(() => gdpr.cleanupOldData(), 24 * 60 * 60 * 1000);
Data Anonymization
class LocationAnonymizer {
// Reduce precision to protect privacy
static anonymizeLocation(lat, lng, precision = 0.01) {
return {
lat: Math.round(lat / precision) * precision,
lng: Math.round(lng / precision) * precision
};
}
// Add noise to location data
static addNoise(lat, lng, radiusMeters = 100) {
const angle = Math.random() * 2 * Math.PI;
const radius = Math.random() * radiusMeters;
const latOffset = (radius * Math.cos(angle)) / 111000;
const lngOffset = (radius * Math.sin(angle)) / (111000 * Math.cos(lat * Math.PI / 180));
return {
lat: lat + latOffset,
lng: lng + lngOffset
};
}
// Use k-anonymity: group locations
static generalizeToArea(lat, lng, gridSize = 0.1) {
const gridLat = Math.floor(lat / gridSize) * gridSize;
const gridLng = Math.floor(lng / gridSize) * gridSize;
return {
lat: gridLat + (gridSize / 2),
lng: gridLng + (gridSize / 2),
area: `${gridLat},${gridLng}`
};
}
}
Testing Hardware APIs
Mocking Geolocation
class GeolocationMock {
constructor() {
this.position = {
coords: {
latitude: 40.7128,
longitude: -74.0060,
accuracy: 10,
altitude: null,
altitudeAccuracy: null,
heading: null,
speed: null
},
timestamp: Date.now()
};
this.watchCallbacks = [];
}
install() {
// Save original
this.original = navigator.geolocation;
// Replace with mock
navigator.geolocation = {
getCurrentPosition: (success, error) => {
setTimeout(() => success(this.position), 100);
},
watchPosition: (success, error) => {
const id = this.watchCallbacks.length;
this.watchCallbacks.push(success);
setTimeout(() => success(this.position), 100);
return id;
},
clearWatch: (id) => {
delete this.watchCallbacks[id];
}
};
}
setPosition(lat, lng, accuracy = 10) {
this.position.coords.latitude = lat;
this.position.coords.longitude = lng;
this.position.coords.accuracy = accuracy;
this.position.timestamp = Date.now();
// Trigger watch callbacks
this.watchCallbacks.forEach(callback => {
if (callback) callback(this.position);
});
}
simulateError(code = 1) {
// Override to return error
navigator.geolocation.getCurrentPosition = (success, error) => {
setTimeout(() => error({
code,
message: 'Simulated error',
PERMISSION_DENIED: 1,
POSITION_UNAVAILABLE: 2,
TIMEOUT: 3
}), 100);
};
}
restore() {
navigator.geolocation = this.original;
}
}
// Usage in tests
const mock = new GeolocationMock();
mock.install();
// Test with mock location
mock.setPosition(51.5074, -0.1278); // London
navigator.geolocation.getCurrentPosition((pos) => {
console.assert(pos.coords.latitude === 51.5074);
});
mock.restore();
Testing with Jest
// geolocation.test.js
describe('Geolocation', () => {
let mockGeolocation;
beforeEach(() => {
mockGeolocation = {
getCurrentPosition: jest.fn(),
watchPosition: jest.fn(),
clearWatch: jest.fn()
};
global.navigator.geolocation = mockGeolocation;
});
afterEach(() => {
jest.clearAllMocks();
});
test('should request current position', async () => {
const mockPosition = {
coords: {
latitude: 40.7128,
longitude: -74.0060,
accuracy: 10
},
timestamp: Date.now()
};
mockGeolocation.getCurrentPosition.mockImplementation((success) => {
success(mockPosition);
});
const result = await getCurrentLocation();
expect(mockGeolocation.getCurrentPosition).toHaveBeenCalled();
expect(result.coords.latitude).toBe(40.7128);
});
test('should handle permission denied', async () => {
const mockError = {
code: 1,
message: 'User denied Geolocation'
};
mockGeolocation.getCurrentPosition.mockImplementation((success, error) => {
error(mockError);
});
await expect(getCurrentLocation()).rejects.toThrow();
});
});
// Helper function
function getCurrentLocation() {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
}
Testing MediaStream
describe('Camera Access', () => {
let mockGetUserMedia;
beforeEach(() => {
mockGetUserMedia = jest.fn();
global.navigator.mediaDevices = {
getUserMedia: mockGetUserMedia
};
});
test('should request camera access', async () => {
const mockStream = {
getTracks: () => [{
kind: 'video',
stop: jest.fn()
}]
};
mockGetUserMedia.mockResolvedValue(mockStream);
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
expect(mockGetUserMedia).toHaveBeenCalledWith({ video: true });
expect(stream).toBeDefined();
});
test('should handle camera permission denied', async () => {
const mockError = new Error('Permission denied');
mockError.name = 'NotAllowedError';
mockGetUserMedia.mockRejectedValue(mockError);
await expect(
navigator.mediaDevices.getUserMedia({ video: true })
).rejects.toThrow('Permission denied');
});
});
Browser DevTools Testing
// Chrome DevTools: Override geolocation
// 1. Open DevTools (F12)
// 2. Press Ctrl+Shift+P (Cmd+Shift+P on Mac)
// 3. Type "Show Sensors"
// 4. Select location or enter custom coordinates
// Programmatically in DevTools Console:
const position = {
coords: {
latitude: 37.7749,
longitude: -122.4194,
accuracy: 10
},
timestamp: Date.now()
};
// Override getCurrentPosition
const originalGetPosition = navigator.geolocation.getCurrentPosition;
navigator.geolocation.getCurrentPosition = function(success, error) {
success(position);
};
Accessibility Considerations
Voice Control Integration
class VoiceController {
constructor() {
this.recognition = null;
this.commands = new Map();
}
async init() {
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
console.error('Speech recognition not supported');
return false;
}
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
this.recognition = new SpeechRecognition();
this.recognition.continuous = true;
this.recognition.interimResults = false;
this.recognition.lang = 'en-US';
this.recognition.onresult = (event) => {
const last = event.results.length - 1;
const command = event.results[last][0].transcript.toLowerCase().trim();
console.log('Voice command:', command);
this.executeCommand(command);
};
this.recognition.onerror = (event) => {
console.error('Speech recognition error:', event.error);
};
return true;
}
registerCommand(phrase, callback) {
this.commands.set(phrase.toLowerCase(), callback);
}
executeCommand(command) {
for (const [phrase, callback] of this.commands) {
if (command.includes(phrase)) {
callback(command);
this.speak(`Executing ${phrase}`);
return;
}
}
this.speak('Command not recognized');
}
speak(text) {
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
window.speechSynthesis.speak(utterance);
}
}
start() {
this.recognition?.start();
}
stop() {
this.recognition?.stop();
}
}
// Usage
const voice = new VoiceController();
await voice.init();
// Register location commands
voice.registerCommand('show my location', () => {
navigator.geolocation.getCurrentPosition((pos) => {
const { latitude, longitude } = pos.coords;
voice.speak(`Your location is latitude ${latitude.toFixed(2)}, longitude ${longitude.toFixed(2)}`);
});
});
voice.registerCommand('find nearby', () => {
// Find nearby places
voice.speak('Searching for nearby locations');
});
voice.start();
Screen Reader Support
class AccessibleLocationUI {
announceLocation(position) {
const { latitude, longitude, accuracy } = position.coords;
// Create live region for screen readers
const announcement = document.createElement('div');
announcement.setAttribute('role', 'status');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only'; // Visually hidden
announcement.textContent = `Location updated: ` +
`Latitude ${latitude.toFixed(4)}, ` +
`Longitude ${longitude.toFixed(4)}, ` +
`Accuracy ${Math.round(accuracy)} meters`;
document.body.appendChild(announcement);
// Remove after announcement
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}
announcePermissionRequest() {
const announcement = this.createAnnouncement(
'Requesting location permission. Please allow access to continue.'
);
document.body.appendChild(announcement);
}
announceError(error) {
let message = 'Location error: ';
switch (error.code) {
case 1:
message += 'Permission denied. Please enable location access in settings.';
break;
case 2:
message += 'Position unavailable. Please check your connection.';
break;
case 3:
message += 'Request timed out. Please try again.';
break;
}
const announcement = this.createAnnouncement(message, 'assertive');
document.body.appendChild(announcement);
}
createAnnouncement(text, priority = 'polite') {
const div = document.createElement('div');
div.setAttribute('role', 'status');
div.setAttribute('aria-live', priority);
div.setAttribute('aria-atomic', 'true');
div.className = 'sr-only';
div.textContent = text;
return div;
}
}
// CSS for screen reader only content
const css = `
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
`;
Keyboard Navigation
class KeyboardAccessibleMap {
constructor(mapElement) {
this.map = mapElement;
this.focusIndex = 0;
this.markers = [];
this.setupKeyboardControls();
}
setupKeyboardControls() {
this.map.setAttribute('tabindex', '0');
this.map.setAttribute('role', 'application');
this.map.setAttribute('aria-label', 'Interactive map. Use arrow keys to navigate.');
this.map.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
this.panMap(0, -10);
break;
case 'ArrowDown':
e.preventDefault();
this.panMap(0, 10);
break;
case 'ArrowLeft':
e.preventDefault();
this.panMap(-10, 0);
break;
case 'ArrowRight':
e.preventDefault();
this.panMap(10, 0);
break;
case '+':
case '=':
e.preventDefault();
this.zoomIn();
break;
case '-':
e.preventDefault();
this.zoomOut();
break;
case 'Tab':
this.focusNextMarker();
break;
case 'Enter':
case ' ':
e.preventDefault();
this.activateCurrentMarker();
break;
}
});
}
panMap(x, y) {
console.log(`Panning map: ${x}, ${y}`);
this.announceMapMovement(x, y);
}
zoomIn() {
console.log('Zooming in');
this.announce('Zoomed in');
}
zoomOut() {
console.log('Zooming out');
this.announce('Zoomed out');
}
focusNextMarker() {
if (this.markers.length === 0) return;
this.focusIndex = (this.focusIndex + 1) % this.markers.length;
const marker = this.markers[this.focusIndex];
this.announce(`Marker ${this.focusIndex + 1} of ${this.markers.length}: ${marker.label}`);
}
activateCurrentMarker() {
if (this.markers.length === 0) return;
const marker = this.markers[this.focusIndex];
console.log('Activated marker:', marker);
this.announce(`Opened ${marker.label}`);
}
announce(text) {
const announcement = document.createElement('div');
announcement.setAttribute('role', 'status');
announcement.setAttribute('aria-live', 'polite');
announcement.className = 'sr-only';
announcement.textContent = text;
document.body.appendChild(announcement);
setTimeout(() => document.body.removeChild(announcement), 1000);
}
announceMapMovement(x, y) {
const direction = x > 0 ? 'right' : x < 0 ? 'left' : y > 0 ? 'down' : 'up';
this.announce(`Moved map ${direction}`);
}
}
Real-World Use Cases
Location-Based Services
class LocationBasedService {
async findNearbyPlaces(radius = 1000) {
const position = await this.getCurrentLocation();
const { latitude, longitude } = position.coords;
const response = await fetch(
`/api/places?lat=${latitude}&lon=${longitude}&radius=${radius}`
);
return response.json();
}
async getCurrentLocation() {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
}
async getLocationBasedRecommendations() {
const position = await this.getCurrentLocation();
const { latitude, longitude } = position.coords;
// Get weather
const weather = await fetch(
`https://api.weather.com/v3/wx/conditions/current?` +
`geocode=${latitude},${longitude}&format=json`
).then(r => r.json());
// Get local time
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const localTime = new Date().toLocaleTimeString('en-US', { timezone });
// Get nearby events
const events = await fetch(
`/api/events?lat=${latitude}&lng=${longitude}`
).then(r => r.json());
return {
weather,
localTime,
events,
recommendations: this.generateRecommendations(weather, localTime, events)
};
}
generateRecommendations(weather, time, events) {
const recommendations = [];
if (weather.temperature > 25) {
recommendations.push('Visit a nearby ice cream shop');
}
const hour = new Date().getHours();
if (hour >= 11 && hour <= 14) {
recommendations.push('Check out lunch specials nearby');
}
if (events.length > 0) {
recommendations.push(`${events.length} events happening nearby`);
}
return recommendations;
}
}
Augmented Reality
class ARExperience {
constructor() {
this.camera = null;
this.orientation = null;
this.location = null;
}
async initAR() {
// Request camera and motion permissions
this.camera = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' } // Back camera
});
// Request motion permission (iOS)
if (typeof DeviceMotionEvent.requestPermission === 'function') {
const motionGranted = await DeviceMotionEvent.requestPermission();
if (motionGranted !== 'granted') {
throw new Error('Motion permission denied');
}
}
// Request location for AR anchoring
this.location = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
this.setupARView();
}
setupARView() {
const video = document.getElementById('ar-video');
video.srcObject = this.camera;
// Track device orientation for AR positioning
window.addEventListener('deviceorientation', (event) => {
this.orientation = {
alpha: event.alpha,
beta: event.beta,
gamma: event.gamma
};
this.updateARObjects();
});
// Track device motion for better AR stability
window.addEventListener('devicemotion', (event) => {
const { x, y, z } = event.acceleration;
this.updateARStability({ x, y, z });
});
}
updateARObjects() {
if (!this.orientation) return;
// Calculate AR object positions based on:
// 1. Device orientation (where user is looking)
// 2. User's GPS location
// 3. AR object's GPS location
const arObjects = document.querySelectorAll('.ar-object');
arObjects.forEach(obj => {
const bearing = this.calculateBearing(
this.location.coords.latitude,
this.location.coords.longitude,
obj.dataset.lat,
obj.dataset.lng
);
const distance = this.calculateDistance(
this.location.coords.latitude,
this.location.coords.longitude,
obj.dataset.lat,
obj.dataset.lng
);
// Adjust object position based on device orientation
const relativeBearing = bearing - this.orientation.alpha;
this.positionARObject(obj, relativeBearing, distance, this.orientation.beta);
});
}
calculateBearing(lat1, lon1, lat2, lon2) {
const dLon = (lon2 - lon1) * Math.PI / 180;
const y = Math.sin(dLon) * Math.cos(lat2 * Math.PI / 180);
const x = Math.cos(lat1 * Math.PI / 180) * Math.sin(lat2 * Math.PI / 180) -
Math.sin(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.cos(dLon);
const bearing = Math.atan2(y, x) * 180 / Math.PI;
return (bearing + 360) % 360;
}
calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371000; // meters
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
positionARObject(obj, bearing, distance, tilt) {
// Position AR object in viewport based on bearing and distance
const screenWidth = window.innerWidth;
const fieldOfView = 60; // degrees
// Calculate horizontal position
const x = (bearing / fieldOfView) * screenWidth + (screenWidth / 2);
// Calculate vertical position based on device tilt
const y = window.innerHeight / 2 + (tilt * 5);
// Scale based on distance
const scale = Math.max(0.3, Math.min(2, 100 / distance));
obj.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
// Show/hide based on whether it's in view
obj.style.display = (Math.abs(bearing) < fieldOfView / 2) ? 'block' : 'none';
// Update distance label
const label = obj.querySelector('.distance');
if (label) {
label.textContent = `${Math.round(distance)}m`;
}
}
updateARStability(acceleration) {
// Use accelerometer data to stabilize AR view
const magnitude = Math.sqrt(
acceleration.x ** 2 +
acceleration.y ** 2 +
acceleration.z ** 2
);
// If device is moving too much, show stability warning
if (magnitude > 15) {
this.showStabilityWarning();
}
}
showStabilityWarning() {
const warning = document.getElementById('stability-warning');
if (warning) {
warning.style.display = 'block';
setTimeout(() => {
warning.style.display = 'none';
}, 2000);
}
}
cleanup() {
this.camera?.getTracks().forEach(track => track.stop());
window.removeEventListener('deviceorientation', this.updateARObjects);
window.removeEventListener('devicemotion', this.updateARStability);
}
}
// Usage
const arApp = new ARExperience();
await arApp.initAR();
Fitness Tracking
class FitnessTracker {
constructor() {
this.session = null;
this.steps = 0;
this.distance = 0;
this.calories = 0;
this.startTime = null;
}
async startActivity(activityType = 'running') {
this.startTime = Date.now();
this.session = {
type: activityType,
route: [],
steps: 0,
distance: 0,
calories: 0,
heartRate: []
};
// Start location tracking
const routeTracker = new RouteTracker();
routeTracker.start();
// Start step detection
this.startStepDetection();
// Monitor battery for power management
const battery = await navigator.getBattery();
this.adjustTrackingForBattery(battery.level);
battery.addEventListener('levelchange', () => {
this.adjustTrackingForBattery(battery.level);
});
}
startStepDetection() {
let lastStep = 0;
const stepThreshold = 12; // Acceleration threshold for step detection
window.addEventListener('devicemotion', (event) => {
const { x, y, z } = event.acceleration;
const magnitude = Math.sqrt(x**2 + y**2 + z**2);
// Detect step
if (magnitude > stepThreshold) {
const now = Date.now();
if (now - lastStep > 300) { // Minimum 300ms between steps
this.steps++;
lastStep = now;
this.calculateMetrics();
}
}
});
}
calculateMetrics() {
// Calculate distance (rough estimate)
const strideLength = 0.78; // meters
this.distance = this.steps * strideLength;
// Calculate calories burned
const caloriesPerStep = 0.04;
this.calories = this.steps * caloriesPerStep;
// Calculate pace
const duration = (Date.now() - this.startTime) / 60000; // minutes
const pace = duration / (this.distance / 1000); // min/km
this.updateUI({
steps: this.steps,
distance: this.distance,
calories: this.calories,
pace: pace
});
}
adjustTrackingForBattery(level) {
if (level < 0.2) {
// Low battery: reduce tracking frequency
console.log('Low battery: reducing tracking frequency');
} else if (level < 0.5) {
// Medium battery: normal tracking
console.log('Medium battery: normal tracking');
} else {
// High battery: high accuracy tracking
console.log('High battery: high accuracy tracking');
}
}
async connectHeartRateMonitor() {
if (!('bluetooth' in navigator)) {
console.error('Web Bluetooth not supported');
return;
}
try {
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService('heart_rate');
const characteristic = await service.getCharacteristic('heart_rate_measurement');
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', (event) => {
const heartRate = event.target.value.getUint8(1);
this.session.heartRate.push({
value: heartRate,
timestamp: Date.now()
});
document.getElementById('heart-rate').textContent = heartRate;
});
} catch (error) {
console.error('Heart rate monitor connection failed:', error);
}
}
updateUI(metrics) {
document.getElementById('steps').textContent = metrics.steps;
document.getElementById('distance').textContent = `${(metrics.distance / 1000).toFixed(2)} km`;
document.getElementById('calories').textContent = Math.round(metrics.calories);
document.getElementById('pace').textContent = `${metrics.pace.toFixed(2)} min/km`;
}
async stopActivity() {
const duration = (Date.now() - this.startTime) / 1000;
return {
...this.session,
duration,
steps: this.steps,
distance: this.distance,
calories: this.calories,
avgPace: (duration / 60) / (this.distance / 1000)
};
}
}
// Usage
const fitness = new FitnessTracker();
await fitness.startActivity('running');
await fitness.connectHeartRateMonitor();
// Later...
const summary = await fitness.stopActivity();
console.log('Workout summary:', summary);
Smart Home Integration
class SmartHomeController {
async detectHomeArrival() {
const homeLocation = {
lat: 40.7128,
lng: -74.0060,
radius: 100 // meters
};
const geofence = new Geofence(homeLocation, homeLocation.radius);
geofence.onEnter(async () => {
console.log('Arrived home');
await this.triggerHomeAutomation();
});
geofence.onExit(() => {
console.log('Left home');
this.triggerAwayMode();
});
}
async triggerHomeAutomation() {
// Turn on lights, adjust thermostat, etc.
await fetch('/api/smart-home/arrive', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
timestamp: Date.now(),
action: 'arrive'
})
});
// Trigger haptic feedback
navigator.vibrate([200, 100, 200]);
}
async triggerAwayMode() {
await fetch('/api/smart-home/away', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
timestamp: Date.now(),
action: 'away'
})
});
}
}
Browser Compatibility
| API | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| Geolocation | โ | โ | โ | โ |
| MediaStream | โ | โ | โ | โ |
| DeviceMotion | โ | โ | โ | โ |
| DeviceOrientation | โ | โ | โ | โ |
| Battery Status | โ | โ | โ | โ |
| Vibration | โ | โ | โ ๏ธ | โ |
Troubleshooting Common Issues
Geolocation Not Working
// Check for common issues
function debugGeolocation() {
if (!('geolocation' in navigator)) {
console.error('Geolocation not supported');
return;
}
if (!window.isSecureContext) {
console.error('HTTPS required');
return;
}
navigator.geolocation.getCurrentPosition(
(pos) => console.log('Success:', pos),
(err) => {
switch (err.code) {
case 1:
console.error('Permission denied');
break;
case 2:
console.error('Position unavailable');
break;
case 3:
console.error('Timeout');
break;
}
}
);
}
Camera Permission Issues
// Handle camera permission errors
async function debugCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
console.log('Camera access granted');
} catch (error) {
if (error.name === 'NotAllowedError') {
console.error('Camera permission denied');
} else if (error.name === 'NotFoundError') {
console.error('No camera found');
} else {
console.error('Camera error:', error);
}
}
}
Future Trends
Emerging APIs to watch:
- Web Bluetooth: Connect to Bluetooth devices
- Web USB: Access USB devices
- Ambient Light Sensor: Detect ambient light levels
- Proximity Sensor: Detect nearby objects
- Magnetometer: Access compass data
- Barometer: Detect altitude changes
Conclusion
Geolocation and hardware APIs unlock powerful capabilities for web applications, enabling experiences that rival native applications. By implementing them thoughtfullyโwith proper permissions, security, performance optimization, and user experience considerationsโyou can create immersive, context-aware applications that users love.
Key Takeaways:
- Always request permissions explicitly: Users should know what you’re accessing and why
- Require HTTPS: Most hardware APIs require secure contexts for security
- Handle errors gracefully: Provide clear fallbacks when APIs aren’t available or permissions are denied
- Respect privacy: Encrypt sensitive data, implement GDPR compliance, and minimize data collection
- Optimize for battery: Adjust tracking frequency based on battery level and user context
- Test thoroughly: Test on real devices with various permissions states and scenarios
- Progressive enhancement: Build features that work without these APIs as fallback
- Consider accessibility: Ensure hardware features are accessible via alternative methods
Production Checklist:
- HTTPS enabled on all pages
- Permission requests with clear explanations
- Error handling for all permission states
- GDPR-compliant consent management
- Data encryption for sensitive information
- Battery-aware resource management
- Offline support with IndexedDB caching
- Accessibility features (voice, keyboard, screen reader)
- Cross-browser testing (Chrome, Firefox, Safari, Edge)
- Mobile testing on iOS and Android
- Performance monitoring and optimization
- Security audit completed
- Privacy policy updated
- User documentation provided
Performance Best Practices:
- Throttle sensor events to 100ms intervals
- Debounce orientation changes
- Use
maximumAgeto reduce GPS queries - Disable
enableHighAccuracywhen not needed - Stop tracking when app is backgrounded
- Clean up event listeners and streams
- Cache location data for offline use
- Batch API requests to reduce network usage
Security Checklist:
- Never store plaintext location data
- Use Web Crypto API for encryption
- Implement data retention policies
- Allow users to delete their data
- Anonymize data when possible
- Add noise to reduce precision
- Audit third-party dependencies
- Monitor for security vulnerabilities
- Implement rate limiting on APIs
- Use CSP headers to prevent XSS
Future Trends:
The web platform continues to evolve, bringing more device capabilities to developers:
- Web Bluetooth: Connect to Bluetooth devices (fitness trackers, smart watches, IoT devices)
- Web USB: Access USB devices (Arduino, hardware development tools)
- Web NFC: Read and write NFC tags (payments, access control, smart posters)
- Ambient Light Sensor: Detect ambient light levels for auto brightness
- Proximity Sensor: Detect nearby objects (during phone calls, etc.)
- Magnetometer: Access compass data for better navigation
- Barometer: Detect altitude changes for fitness tracking
- WebXR: Immersive VR and AR experiences
- Shape Detection API: Detect faces, barcodes, text in images
- Web Serial API: Communicate with serial devices
Browser Support Improving:
Major browsers are actively implementing and improving hardware APIs:
- Chrome: Leading in experimental API support
- Firefox: Strong privacy-focused implementations
- Safari: Catching up with iOS 13+ improvements
- Edge: Chromium-based, matching Chrome support
Resources for Further Learning:
- MDN Web Docs - Web APIs
- W3C Geolocation API Specification
- Web.dev - Device APIs
- Can I Use - Browser compatibility tables
- WebBluetooth Community Group
- Google Developers - Access Hardware
Community and Support:
- Stack Overflow
- GitHub Web Platform Tests
- WICG - Web Incubator Community Group
Tools for Development:
- Chrome DevTools Sensors panel
- Firefox Developer Tools
- Remote debugging for mobile devices
- Geolocation spoofing extensions
- Device simulators and emulators
The web platform continues to evolve, bringing more device capabilities to developers. Use these APIs responsibly to create better experiences for your users while respecting their privacy and device resources.
Remember: With great power comes great responsibility. Always prioritize user privacy, security, and experience when implementing hardware APIs. Test thoroughly, handle errors gracefully, and provide clear value to users in exchange for the permissions you request.
Happy coding! ๐
Comments