6. Chapter 6: JavaScript Fundamentals#
6.1. Learning Objectives#
By the end of this chapter, you will understand:
Essential JavaScript concepts for Web GIS development
DOM manipulation for mapping interfaces
Asynchronous programming with geospatial data
Event handling for interactive maps
Modern JavaScript features and best practices
6.2. Modern JavaScript for Web GIS#
JavaScript is the foundation of interactive Web GIS applications. Modern JavaScript (ES6+) provides powerful features that make geospatial development more efficient and maintainable.
6.2.1. Variables and Data Types#
// Essential JavaScript for Web GIS
const MAP_CENTER = [-74.006, 40.7128];
let currentZoom = 12;
// GeoJSON feature structure
const feature = {
type: "Feature",
geometry: {
type: "Point",
coordinates: [-74.006, 40.7128]
},
properties: {
name: "New York City",
population: 8336817
}
};
// Template literals for popups
const popupContent = `
<h3>${feature.properties.name}</h3>
<p>Population: ${feature.properties.population.toLocaleString()}</p>
`;
6.2.2. Functions and Arrow Functions#
// Simple distance calculation using Haversine formula
function calculateDistance(point1, point2) {
const [lon1, lat1] = point1;
const [lon2, lat2] = point2;
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) ** 2 +
Math.cos(lat1 * Math.PI/180) * Math.cos(lat2 * Math.PI/180) *
Math.sin(dLon/2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
// Arrow functions for simple operations
const formatCoordinates = (coords) =>
`${coords[1].toFixed(4)}, ${coords[0].toFixed(4)}`;
const getRestaurants = (features) =>
features.filter(f => f.properties.category === 'restaurant');
### Destructuring and Spread Operator
```javascript
// Destructuring for cleaner code
const [longitude, latitude] = coordinates;
const { name, population } = feature.properties;
// Spread operator for updates
const updatedFeature = {
...feature,
properties: {
...feature.properties,
visited: true
}
};
// Combine arrays
const allFeatures = [...restaurants, ...hotels];
// Function with destructuring
function createPopup({ name, coordinates }) {
return `
<div class="popup">
<h3>${name}</h3>
<small>${formatCoordinates(coordinates)}</small>
</div>
`;
}
6.3. DOM Manipulation#
6.3.1. Selecting and Modifying Elements#
// DOM selection
const mapContainer = document.getElementById('map');
const layerControls = document.querySelector('.layer-controls');
// Create elements
function createLayerControl(layerName, layerId) {
const control = document.createElement('div');
control.innerHTML = `
<input type="checkbox" id="${layerId}" checked>
<label for="${layerId}">${layerName}</label>
`;
return control;
}
// Add layer controls
const layers = ['Restaurants', 'Hotels', 'Attractions'];
layers.forEach((name, index) => {
const control = createLayerControl(name, `layer-${index}`);
layerControls.appendChild(control);
});
// Update map info
function updateMapInfo(zoom, center) {
const infoPanel = document.querySelector('.map-info');
infoPanel.innerHTML = `
<div>Zoom: ${zoom.toFixed(1)}</div>
<div>Center: ${formatCoordinates(center)}</div>
`;
}
const toggleButton = document.querySelector(“.sidebar-toggle”); toggleButton.textContent = sidebar.classList.contains(“collapsed”) ? “>” : “<”; }
### Event Handling
```javascript
// Basic event listeners
document.getElementById("zoom-in").addEventListener("click", () => {
map.zoomIn();
});
document.getElementById("zoom-out").addEventListener("click", () => {
map.zoomOut();
});
// Event delegation for dynamic content
layerControls.addEventListener("change", (event) => {
if (event.target.type === "checkbox") {
const layerId = event.target.id;
const isVisible = event.target.checked;
toggleMapLayer(layerId, isVisible);
}
});
// Custom events for map interactions
function dispatchMapEvent(eventType, data) {
const customEvent = new CustomEvent(eventType, {
detail: data,
bubbles: true,
});
document.dispatchEvent(customEvent);
}
// Listen for custom events
document.addEventListener("featureSelected", (event) => {
const { feature, coordinates } = event.detail;
showFeaturePopup(feature, coordinates);
});
// Keyboard event handling
document.addEventListener("keydown", (event) => {
switch (event.key) {
case "Escape":
closeAllPopups();
break;
case "+":
if (event.ctrlKey) {
event.preventDefault();
map.zoomIn();
}
break;
case "-":
if (event.ctrlKey) {
event.preventDefault();
map.zoomOut();
}
break;
}
});
6.4. Fetching GeoJSON Data#
6.4.1. Modern Fetch API#
// Simple data loading
async function loadGeoJSON(url) {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error('Error loading data:', error);
return null;
}
}
// Load multiple datasets
async function loadMapData() {
const [restaurants, hotels] = await Promise.all([
loadGeoJSON('/api/restaurants.geojson'),
loadGeoJSON('/api/hotels.geojson')
]);
return { restaurants, hotels };
}
// Usage
const mapData = await loadMapData();
if (mapData.restaurants) {
map.addSource('restaurants', {
type: 'geojson',
data: mapData.restaurants
});
}
6.4.2. Data Processing and Validation#
// GeoJSON validation
function isValidGeoJSON(data) {
return data?.type === "FeatureCollection" &&
Array.isArray(data.features) &&
data.features.every(f => f.type === "Feature" && f.geometry && f.properties);
}
// Data processing pipeline
function processGeoJSONData(data) {
if (!isValidGeoJSON(data)) {
throw new Error("Invalid GeoJSON data");
}
return {
...data,
features: data.features
.filter(feature => feature.geometry)
.map(feature => ({
...feature,
properties: {
...feature.properties,
id: feature.properties.id || Math.random().toString(36).substr(2, 9),
processedAt: new Date().toISOString()
}
}))
};
}
// Utility functions
const groupFeaturesByProperty = (features, property) =>
features.reduce((groups, feature) => {
const key = feature.properties[property];
groups[key] = groups[key] || [];
groups[key].push(feature);
return groups;
}, {});
const calculateBounds = (features) => {
if (features.length === 0) return null;
let minLng = Infinity, minLat = Infinity;
let maxLng = -Infinity, maxLat = -Infinity;
features.forEach(feature => {
const [lng, lat] = feature.geometry.coordinates;
minLng = Math.min(minLng, lng);
minLat = Math.min(minLat, lat);
maxLng = Math.max(maxLng, lng);
maxLat = Math.max(maxLat, lat);
});
return [
[minLng, minLat],
[maxLng, maxLat],
];
};
6.5. Asynchronous Programming#
6.5.1. Promises and Async/Await#
// Simple geocoding
async function searchLocation(address) {
try {
const response = await fetch(`/api/geocode?q=${address}`);
const result = await response.json();
// Move map to location
map.flyTo({
center: result.coordinates,
zoom: 14
});
// Add marker
new maplibregl.Marker()
.setLngLat(result.coordinates)
.addTo(map);
} catch (error) {
console.error('Search failed:', error);
}
}
// Initialize map with data
async function initializeMap() {
try {
// Load data and initialize map
const data = await loadGeoJSON('/api/places.geojson');
map.on('load', () => {
map.addSource('places', {
type: 'geojson',
data: data
});
map.addLayer({
id: 'places',
type: 'circle',
source: 'places'
});
});
} catch (error) {
console.error('Initialization failed:', error);
}
}
}
function updateProgress(progressBar, percent, message) {
progressBar.style.width = ${percent}%;
progressBar.setAttribute(“aria-valuenow”, percent);
const statusText = document.querySelector(“.progress-status”); if (statusText) { statusText.textContent = message; } }
### Error Handling and Retry Logic
```javascript
// Retry mechanism for failed requests
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.warn(`Attempt ${attempt} failed:`, error.message);
if (attempt === maxRetries) {
throw new Error(
`All ${maxRetries} attempts failed. Last error: ${error.message}`
);
}
// Exponential backoff
const delayMs = Math.pow(2, attempt) * 1000;
await delay(delayMs);
}
}
}
// Graceful error handling for user interface
class MapDataManager {
constructor() {
this.cache = new Map();
this.loadingStates = new Map();
}
async loadLayer(layerId, url) {
// Check cache first
if (this.cache.has(layerId)) {
return this.cache.get(layerId);
}
// Prevent duplicate requests
if (this.loadingStates.has(layerId)) {
return this.loadingStates.get(layerId);
}
const loadingPromise = this._loadLayerData(layerId, url);
this.loadingStates.set(layerId, loadingPromise);
try {
const data = await loadingPromise;
this.cache.set(layerId, data);
return data;
} finally {
this.loadingStates.delete(layerId);
}
}
async _loadLayerData(layerId, url) {
try {
this._showLoadingState(layerId);
const data = await fetchWithRetry(url);
this._hideLoadingState(layerId);
return processGeoJSONData(data);
} catch (error) {
this._showErrorState(layerId, error.message);
throw error;
}
}
_showLoadingState(layerId) {
const control = document.querySelector(`[data-layer="${layerId}"]`);
if (control) {
control.classList.add("loading");
}
}
_hideLoadingState(layerId) {
const control = document.querySelector(`[data-layer="${layerId}"]`);
if (control) {
control.classList.remove("loading");
}
}
_showErrorState(layerId, message) {
const control = document.querySelector(`[data-layer="${layerId}"]`);
if (control) {
control.classList.add("error");
control.title = `Error loading layer: ${message}`;
}
}
}
6.6. Event-Driven Architecture#
6.6.1. Custom Event System#
// Event emitter for map interactions
class MapEventEmitter {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(
(cb) => cb !== callback
);
}
}
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach((callback) => {
try {
callback(data);
} catch (error) {
console.error(`Error in event handler for ${eventName}:`, error);
}
});
}
}
once(eventName, callback) {
const onceCallback = (data) => {
callback(data);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
}
}
// Usage example
const mapEvents = new MapEventEmitter();
// Register event handlers
mapEvents.on("featureClick", (feature) => {
console.log("Feature clicked:", feature.properties.name);
showFeatureDetails(feature);
});
mapEvents.on("layerToggle", ({ layerId, visible }) => {
console.log(`Layer ${layerId} visibility: ${visible}`);
updateLayerVisibility(layerId, visible);
});
// Map interaction handlers
map.on("click", (e) => {
const features = map.queryRenderedFeatures(e.point);
if (features.length > 0) {
mapEvents.emit("featureClick", features[0]);
}
});
// Layer control handlers
document.addEventListener("change", (e) => {
if (e.target.matches(".layer-checkbox")) {
mapEvents.emit("layerToggle", {
layerId: e.target.dataset.layer,
visible: e.target.checked,
});
}
});
6.7. Modern JavaScript Features#
6.7.1. Modules and Imports#
// utils/geometry.js
export function calculateDistance(point1, point2) {
// Implementation here
}
export function calculateArea(polygon) {
// Implementation here
}
export const EARTH_RADIUS = 6371;
// utils/formatting.js
export default function formatCoordinates(coords, precision = 4) {
return coords.map((coord) => coord.toFixed(precision)).join(", ");
}
// main.js
import formatCoordinates from "./utils/formatting.js";
import { calculateDistance, EARTH_RADIUS } from "./utils/geometry.js";
// Dynamic imports for code splitting
async function loadAdvancedFeatures() {
const { AdvancedAnalytics } = await import("./modules/analytics.js");
return new AdvancedAnalytics();
}
6.7.2. Classes and Modern Syntax#
// Modern class syntax for map components
class LayerManager {
#layers = new Map(); // Private field
constructor(map) {
this.map = map;
this.visible = new Set();
}
addLayer(id, config) {
const layer = {
id,
...config,
visible: config.visible ?? true,
};
this.#layers.set(id, layer);
if (layer.visible) {
this.#showLayer(layer);
}
}
toggleLayer(id) {
const layer = this.#layers.get(id);
if (!layer) return false;
layer.visible = !layer.visible;
if (layer.visible) {
this.#showLayer(layer);
} else {
this.#hideLayer(layer);
}
return layer.visible;
}
#showLayer(layer) {
// Private method
this.map.addSource(layer.id, layer.source);
this.map.addLayer(layer.style);
this.visible.add(layer.id);
}
#hideLayer(layer) {
this.map.removeLayer(layer.id);
this.map.removeSource(layer.id);
this.visible.delete(layer.id);
}
get visibleLayers() {
return Array.from(this.visible);
}
}
6.8. Summary#
Modern JavaScript provides powerful tools for building interactive Web GIS applications. Understanding asynchronous programming, DOM manipulation, and event handling is essential for creating responsive mapping interfaces. These fundamentals form the foundation for working with mapping libraries and building complex geospatial applications.
The next chapter will introduce TypeScript, adding static typing to improve code quality and developer experience in Web GIS projects.
6.9. Exercises#
6.9.1. Exercise 6.1: DOM Manipulation for Map Controls#
Objective: Create interactive map controls using vanilla JavaScript DOM manipulation.
Instructions:
Build a custom control panel with the following features:
Layer visibility toggles
Zoom controls (+/- buttons)
Map style switcher
Search input with autocomplete
Implement using modern JavaScript:
Use ES6+ features (arrow functions, destructuring, template literals)
Implement event delegation for dynamic content
Use modern DOM APIs (querySelector, addEventListener)
Add interactive feedback:
Loading states for asynchronous operations
Error handling with user-friendly messages
Keyboard navigation support
Deliverable: A functional control panel that demonstrates proficiency with DOM manipulation.
6.9.2. Exercise 6.2: Asynchronous Data Loading#
Objective: Implement robust data loading patterns for Web GIS applications.
Instructions:
Create a data loading system that handles:
Loading GeoJSON from multiple sources
Handling loading states and errors
Caching loaded data
Retry logic for failed requests
Implement different loading patterns:
Sequential loading (load one dataset at a time)
Parallel loading (load multiple datasets simultaneously)
Progressive loading (load basic data first, then details)
Add user experience features:
Progress indicators
Graceful error handling
Offline detection and handling
Deliverable: A data loading module with comprehensive error handling and user feedback.
6.9.3. Exercise 6.3: Event-Driven Architecture#
Objective: Implement a custom event system for map application communication.
Instructions:
Create a custom event emitter class that can:
Register and unregister event listeners
Emit events with data
Support one-time listeners
Handle namespaced events
Implement application-wide communication:
Map interaction events (click, zoom, move)
Data update events
User interface state changes
Error and notification events
Create modular components that communicate through events:
Map component
Control panel component
Data layer component
Notification component
Deliverable: A modular application architecture using custom events for communication.
6.9.4. Exercise 6.4: Geospatial Calculations#
Objective: Implement common geospatial calculations in JavaScript.
Instructions:
Implement distance calculations:
Haversine formula for great-circle distance
Vincenty formula for more accurate ellipsoid calculations
Simple Euclidean distance for projected coordinates
Create spatial query functions:
Point-in-polygon testing
Line intersection detection
Buffer zone calculations
Bounding box operations
Build a spatial analysis toolkit:
Area calculations for polygons
Centroid calculations
Bearing calculations between points
Coordinate system transformations
Deliverable: A spatial analysis library with comprehensive test cases.
6.9.5. Exercise 6.5: Performance Optimization#
Objective: Optimize JavaScript performance for large datasets and complex operations.
Instructions:
Identify performance bottlenecks:
Use browser profiling tools
Measure execution times for different operations
Test with increasingly large datasets
Implement optimization techniques:
Debouncing for user input events
Throttling for scroll and resize events
Web Workers for CPU-intensive calculations
RequestAnimationFrame for smooth animations
Optimize data processing:
Implement spatial indexing for faster queries
Use efficient data structures
Implement lazy loading for large datasets
Deliverable: Performance analysis report and optimized application code.
6.9.6. Exercise 6.6: Error Handling and Debugging#
Objective: Implement comprehensive error handling and debugging capabilities.
Instructions:
Create a robust error handling system:
Custom error classes for different types of problems
Global error handlers
User-friendly error messages
Error reporting and logging
Implement debugging tools:
Console logging with different levels
Debug mode with additional information
Performance monitoring
State inspection tools
Test error scenarios:
Network failures
Invalid data formats
API rate limiting
Browser compatibility issues
Deliverable: A robust error handling system with comprehensive testing.
6.9.7. Exercise 6.7: Module Organization and Code Structure#
Objective: Organize JavaScript code using modern module patterns.
Instructions:
Restructure a monolithic application into modules:
Separate concerns into different modules
Use ES6 import/export syntax
Implement proper dependency management
Create reusable utilities:
Geospatial calculation utilities
DOM manipulation helpers
Data formatting functions
Configuration management
Implement design patterns:
Module pattern for encapsulation
Observer pattern for event handling
Factory pattern for object creation
Singleton pattern for shared resources
Deliverable: Well-organized, modular JavaScript codebase with clear separation of concerns.
Reflection Questions:
How does modern JavaScript enable more sophisticated Web GIS applications?
What are the trade-offs between client-side and server-side processing for geospatial operations?
How can proper error handling improve the user experience in mapping applications?
What role does code organization play in maintaining large Web GIS projects?