Appendix D: Troubleshooting Guide#
This appendix provides comprehensive troubleshooting guidance for common issues encountered in Web GIS development and deployment. Each section includes problem identification, diagnostic steps, and practical solutions.
Map Display Issues#
Blank or Empty Map#
Symptoms:
Map container displays but no tiles or content appear
Console shows no obvious errors
Map controls may be visible but map area is blank
Common Causes and Solutions:
Invalid API Key or Token#
// Problem: Invalid or missing API key
const map = new maplibregl.Map({
container: "map",
style: "https://api.mapbox.com/styles/v1/mapbox/streets-v11", // Missing access token
center: [-122.4194, 37.7749],
zoom: 12,
});
// Solution: Add proper authentication
const map = new maplibregl.Map({
container: "map",
style: {
version: 8,
sources: {
osm: {
type: "raster",
tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
tileSize: 256,
attribution: "© OpenStreetMap contributors",
},
},
layers: [
{
id: "osm",
source: "osm",
type: "raster",
},
],
},
center: [-122.4194, 37.7749],
zoom: 12,
});
Incorrect Container Setup#
// Problem: Map container not properly sized
// HTML
<div id="map"></div> <!-- No height specified -->
// CSS - Solution: Set explicit dimensions
#map {
width: 100%;
height: 400px; /* Must have explicit height */
}
// JavaScript - Diagnostic check
const container = document.getElementById('map');
console.log('Container dimensions:', {
width: container.offsetWidth,
height: container.offsetHeight
});
if (container.offsetHeight === 0) {
console.error('Map container has no height!');
}
CORS Issues#
// Problem: Cross-origin resource sharing blocked
// Console error: "Access to fetch at '...' from origin '...' has been blocked by CORS policy"
// Solution 1: Use CORS-enabled tile servers
const corsEnabledSources = {
osm: {
type: "raster",
tiles: [
"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
"https://b.tile.openstreetmap.org/{z}/{x}/{y}.png",
"https://c.tile.openstreetmap.org/{z}/{x}/{y}.png",
],
tileSize: 256,
},
};
// Solution 2: Proxy tiles through your server
const proxySource = {
"proxied-tiles": {
type: "raster",
tiles: ["/api/tiles/{z}/{x}/{y}"], // Your server endpoint
tileSize: 256,
},
};
Tiles Not Loading#
Diagnostic Steps:
// Debug tile loading
class TileDebugger {
constructor(map) {
this.map = map;
this.tileLoadCount = 0;
this.tileErrorCount = 0;
this.setupTileDebugging();
}
setupTileDebugging() {
// Monitor tile loading
this.map.on('sourcedata', (e) => {
if (e.sourceDataType === 'tile') {
this.tileLoadCount++;
console.log(`Tiles loaded: ${this.tileLoadCount}`);
}
});
// Monitor tile errors
this.map.on('error', (e) => {
if (e.error && e.error.status) {
this.tileErrorCount++;
console.error(`Tile error (${this.tileErrorCount}):`, {
status: e.error.status,
url: e.error.url,
message: e.error.message
});
}
});
// Monitor source errors
this.map.on('sourcedataloading', (e) => {
console.log('Source loading:', e.sourceId);
});
}
checkTileUrls() {
const style = this.map.getStyle();
Object.entries(style.sources).forEach(([sourceId, source]) => {
if (source.type === 'raster' && source.tiles) {
source.tiles.forEach((tileUrl, index) => {
const testUrl = tileUrl
.replace('{z}', '10')
.replace('{x}', '163')
.replace('{y}', '395');
console.log(`Testing tile URL ${sourceId}-${index}:`, testUrl);
fetch(testUrl, { method: 'HEAD' })
.then(response => {
console.log(`Tile URL ${sourceId}-${index} status:`, response.status);
})
.catch(error => {
console.error(`Tile URL ${sourceId}-${index} failed:`, error);
});
});
}
});
}
getLoadingStats() {
return {
tilesLoaded: this.tileLoadCount,
tileErrors: this.tileErrorCount,
loadedSources: this.map.getStyle().sources
};
}
}
// Usage
const debugger = new TileDebugger(map);
setTimeout(() => {
debugger.checkTileUrls();
console.log('Loading stats:', debugger.getLoadingStats());
}, 2000);
Data Loading Problems#
GeoJSON Not Displaying#
Common Issues and Solutions:
// Problem: Invalid GeoJSON structure
const invalidGeoJSON = {
type: "FeatureCollection",
features: [
{
// Missing "type": "Feature"
geometry: {
type: "Point",
coordinates: [-122.4194, 37.7749],
},
properties: {
name: "San Francisco",
},
},
],
};
// Solution: Validate GeoJSON structure
function validateGeoJSON(geojson) {
const errors = [];
if (!geojson || typeof geojson !== "object") {
errors.push("GeoJSON must be an object");
return errors;
}
if (!geojson.type) {
errors.push("Missing type property");
}
if (geojson.type === "FeatureCollection") {
if (!Array.isArray(geojson.features)) {
errors.push("FeatureCollection must have features array");
} else {
geojson.features.forEach((feature, index) => {
if (feature.type !== "Feature") {
errors.push(`Feature ${index} missing type property`);
}
if (!feature.geometry) {
errors.push(`Feature ${index} missing geometry`);
}
if (!feature.properties) {
errors.push(`Feature ${index} missing properties`);
}
});
}
}
return errors;
}
// Usage
const errors = validateGeoJSON(myGeoJSON);
if (errors.length > 0) {
console.error("GeoJSON validation errors:", errors);
} else {
map.addSource("my-data", {
type: "geojson",
data: myGeoJSON,
});
}
Coordinate System Issues#
// Problem: Wrong coordinate order or projection
const wrongCoordinates = {
type: "Point",
coordinates: [37.7749, -122.4194], // Lat, Lng instead of Lng, Lat
};
// Solution: Coordinate validation and conversion
class CoordinateValidator {
static validateCoordinates(coords, type = "Point") {
if (!Array.isArray(coords)) {
return { valid: false, error: "Coordinates must be an array" };
}
switch (type) {
case "Point":
if (coords.length !== 2) {
return {
valid: false,
error: "Point coordinates must have exactly 2 elements",
};
}
const [lng, lat] = coords;
if (typeof lng !== "number" || typeof lat !== "number") {
return { valid: false, error: "Coordinates must be numbers" };
}
// Check for common coordinate order mistakes
if (Math.abs(lng) > 180) {
return {
valid: false,
error: "Longitude out of range (-180 to 180)",
};
}
if (Math.abs(lat) > 90) {
return { valid: false, error: "Latitude out of range (-90 to 90)" };
}
// Check for likely lat/lng swap
if (Math.abs(lng) <= 90 && Math.abs(lat) > 90) {
return {
valid: false,
error: "Coordinates may be swapped (lat/lng instead of lng/lat)",
suggestion: [lat, lng],
};
}
break;
}
return { valid: true };
}
static fixCommonIssues(geojson) {
if (geojson.type === "FeatureCollection") {
geojson.features.forEach((feature) => {
this.fixFeatureCoordinates(feature);
});
} else if (geojson.type === "Feature") {
this.fixFeatureCoordinates(geojson);
}
return geojson;
}
static fixFeatureCoordinates(feature) {
if (feature.geometry.type === "Point") {
const validation = this.validateCoordinates(
feature.geometry.coordinates,
"Point"
);
if (!validation.valid && validation.suggestion) {
console.warn("Fixed coordinate order for feature:", feature.properties);
feature.geometry.coordinates = validation.suggestion;
}
}
}
}
// Usage
const validation = CoordinateValidator.validateCoordinates([
-122.4194, 37.7749,
]);
if (!validation.valid) {
console.error("Coordinate validation failed:", validation.error);
if (validation.suggestion) {
console.log("Suggested fix:", validation.suggestion);
}
}
Large Dataset Performance#
// Problem: Slow rendering of large datasets
// Solution: Data optimization and chunking
class DataOptimizer {
static simplifyForZoom(geojson, zoom) {
const tolerance = this.getToleranceForZoom(zoom);
return {
...geojson,
features: geojson.features.map((feature) => {
if (
feature.geometry.type === "Polygon" ||
feature.geometry.type === "LineString"
) {
return {
...feature,
geometry: this.simplifyGeometry(feature.geometry, tolerance),
};
}
return feature;
}),
};
}
static getToleranceForZoom(zoom) {
// Higher tolerance (more simplification) at lower zoom levels
return Math.pow(2, 12 - zoom) * 0.0001;
}
static simplifyGeometry(geometry, tolerance) {
// Simplified implementation - use turf.js simplify() in production
if (geometry.type === "LineString") {
return {
...geometry,
coordinates: this.douglasPeucker(geometry.coordinates, tolerance),
};
}
return geometry;
}
static douglasPeucker(points, tolerance) {
// Simplified Douglas-Peucker algorithm
if (points.length <= 2) return points;
// Find the point with maximum distance
let maxDistance = 0;
let maxIndex = 0;
for (let i = 1; i < points.length - 1; i++) {
const distance = this.perpendicularDistance(
points[i],
points[0],
points[points.length - 1]
);
if (distance > maxDistance) {
maxDistance = distance;
maxIndex = i;
}
}
// If max distance is greater than tolerance, recursively simplify
if (maxDistance > tolerance) {
const left = this.douglasPeucker(
points.slice(0, maxIndex + 1),
tolerance
);
const right = this.douglasPeucker(points.slice(maxIndex), tolerance);
return left.slice(0, -1).concat(right);
} else {
return [points[0], points[points.length - 1]];
}
}
static perpendicularDistance(point, lineStart, lineEnd) {
// Calculate perpendicular distance from point to line
const A = point[0] - lineStart[0];
const B = point[1] - lineStart[1];
const C = lineEnd[0] - lineStart[0];
const D = lineEnd[1] - lineStart[1];
const dot = A * C + B * D;
const lenSq = C * C + D * D;
if (lenSq === 0) return Math.sqrt(A * A + B * B);
const param = dot / lenSq;
let xx, yy;
if (param < 0) {
xx = lineStart[0];
yy = lineStart[1];
} else if (param > 1) {
xx = lineEnd[0];
yy = lineEnd[1];
} else {
xx = lineStart[0] + param * C;
yy = lineStart[1] + param * D;
}
const dx = point[0] - xx;
const dy = point[1] - yy;
return Math.sqrt(dx * dx + dy * dy);
}
// Implement data chunking for large datasets
static chunkFeatures(geojson, chunkSize = 1000) {
const chunks = [];
const features = geojson.features;
for (let i = 0; i < features.length; i += chunkSize) {
chunks.push({
type: "FeatureCollection",
features: features.slice(i, i + chunkSize),
});
}
return chunks;
}
}
// Usage with zoom-based optimization
map.on("zoom", () => {
const zoom = map.getZoom();
const optimizedData = DataOptimizer.simplifyForZoom(originalData, zoom);
map.getSource("my-data").setData(optimizedData);
});
Performance Issues#
Slow Map Rendering#
Diagnostic Tools:
class PerformanceProfiler {
constructor(map) {
this.map = map;
this.metrics = {
renderTimes: [],
sourceloadTimes: new Map(),
interactionLag: [],
};
this.setupProfiling();
}
setupProfiling() {
// Monitor render performance
let renderStart = null;
this.map.on("render", () => {
if (!renderStart) {
renderStart = performance.now();
}
});
this.map.on("idle", () => {
if (renderStart) {
const renderTime = performance.now() - renderStart;
this.metrics.renderTimes.push(renderTime);
renderStart = null;
if (renderTime > 16) {
// More than one frame at 60fps
console.warn(`Slow render detected: ${renderTime.toFixed(2)}ms`);
}
}
});
// Monitor data loading
this.map.on("sourcedataloading", (e) => {
this.metrics.sourceloadTimes.set(e.sourceId, performance.now());
});
this.map.on("sourcedata", (e) => {
if (e.sourceDataType === "metadata") {
const startTime = this.metrics.sourceloadTimes.get(e.sourceId);
if (startTime) {
const loadTime = performance.now() - startTime;
console.log(
`Source ${e.sourceId} loaded in ${loadTime.toFixed(2)}ms`
);
}
}
});
// Monitor interaction responsiveness
let interactionStart = null;
["mousedown", "touchstart"].forEach((event) => {
this.map.getContainer().addEventListener(event, () => {
interactionStart = performance.now();
});
});
this.map.on("move", () => {
if (interactionStart) {
const lag = performance.now() - interactionStart;
this.metrics.interactionLag.push(lag);
interactionStart = null;
}
});
}
getMetrics() {
const renderTimes = this.metrics.renderTimes;
const interactionLag = this.metrics.interactionLag;
return {
avgRenderTime:
renderTimes.length > 0
? renderTimes.reduce((a, b) => a + b) / renderTimes.length
: 0,
maxRenderTime: renderTimes.length > 0 ? Math.max(...renderTimes) : 0,
avgInteractionLag:
interactionLag.length > 0
? interactionLag.reduce((a, b) => a + b) / interactionLag.length
: 0,
totalRenders: renderTimes.length,
slowRenders: renderTimes.filter((t) => t > 16).length,
};
}
generateReport() {
const metrics = this.getMetrics();
const report = `
Performance Report:
- Average render time: ${metrics.avgRenderTime.toFixed(2)}ms
- Max render time: ${metrics.maxRenderTime.toFixed(2)}ms
- Slow renders (>16ms): ${metrics.slowRenders}/${metrics.totalRenders}
- Average interaction lag: ${metrics.avgInteractionLag.toFixed(2)}ms
- Performance grade: ${this.getPerformanceGrade(metrics)}
`;
console.log(report);
return metrics;
}
getPerformanceGrade(metrics) {
if (metrics.avgRenderTime < 8 && metrics.avgInteractionLag < 10) {
return "Excellent";
} else if (metrics.avgRenderTime < 16 && metrics.avgInteractionLag < 20) {
return "Good";
} else if (metrics.avgRenderTime < 32 && metrics.avgInteractionLag < 50) {
return "Fair";
} else {
return "Poor";
}
}
}
// Usage
const profiler = new PerformanceProfiler(map);
// Generate report after some interaction
setTimeout(() => {
profiler.generateReport();
}, 10000);
Memory Leaks#
class MemoryMonitor {
constructor() {
this.measurements = [];
this.interval = null;
}
startMonitoring(intervalMs = 5000) {
this.interval = setInterval(() => {
if (performance.memory) {
const memory = performance.memory;
const measurement = {
timestamp: Date.now(),
used: memory.usedJSHeapSize,
total: memory.totalJSHeapSize,
limit: memory.jsHeapSizeLimit,
};
this.measurements.push(measurement);
// Keep only last 100 measurements
if (this.measurements.length > 100) {
this.measurements.shift();
}
// Check for memory leaks
this.checkForLeaks();
}
}, intervalMs);
}
stopMonitoring() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
checkForLeaks() {
if (this.measurements.length < 10) return;
const recent = this.measurements.slice(-10);
const trend = this.calculateTrend(recent.map((m) => m.used));
if (trend > 1024 * 1024) {
// Memory increasing by >1MB
console.warn("Potential memory leak detected:", {
trend: `+${(trend / 1024 / 1024).toFixed(2)}MB`,
current: `${(recent[recent.length - 1].used / 1024 / 1024).toFixed(
2
)}MB`,
});
}
}
calculateTrend(values) {
if (values.length < 2) return 0;
const first = values.slice(0, Math.floor(values.length / 2));
const second = values.slice(Math.floor(values.length / 2));
const firstAvg = first.reduce((a, b) => a + b) / first.length;
const secondAvg = second.reduce((a, b) => a + b) / second.length;
return secondAvg - firstAvg;
}
getCurrentUsage() {
if (performance.memory) {
const memory = performance.memory;
return {
used: (memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + " MB",
total: (memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + " MB",
utilization:
((memory.usedJSHeapSize / memory.totalJSHeapSize) * 100).toFixed(1) +
"%",
};
}
return null;
}
}
// Common memory leak prevention
class MapResourceManager {
constructor(map) {
this.map = map;
this.eventListeners = new Map();
this.intervals = new Set();
this.sources = new Set();
}
// Track event listeners for proper cleanup
addEventListener(target, event, handler) {
target.addEventListener(event, handler);
if (!this.eventListeners.has(target)) {
this.eventListeners.set(target, []);
}
this.eventListeners.get(target).push({ event, handler });
}
// Track intervals for cleanup
setInterval(callback, interval) {
const id = setInterval(callback, interval);
this.intervals.add(id);
return id;
}
// Track map sources
addSource(id, source) {
this.map.addSource(id, source);
this.sources.add(id);
}
// Cleanup all resources
cleanup() {
// Remove event listeners
this.eventListeners.forEach((listeners, target) => {
listeners.forEach(({ event, handler }) => {
target.removeEventListener(event, handler);
});
});
this.eventListeners.clear();
// Clear intervals
this.intervals.forEach((id) => clearInterval(id));
this.intervals.clear();
// Remove map sources
this.sources.forEach((id) => {
if (this.map.getSource(id)) {
this.map.removeSource(id);
}
});
this.sources.clear();
}
}
Database and API Issues#
Database Connection Problems#
// PostgreSQL/PostGIS connection troubleshooting
class DatabaseDiagnostics {
constructor(connectionString) {
this.connectionString = connectionString;
}
async diagnoseConnection() {
const issues = [];
try {
// Test basic connection
const client = new Client(this.connectionString);
await client.connect();
console.log("✓ Database connection successful");
// Test PostGIS extension
const postGISResult = await client.query("SELECT PostGIS_Version();");
if (postGISResult.rows.length > 0) {
console.log(
"✓ PostGIS available:",
postGISResult.rows[0].postgis_version
);
} else {
issues.push("PostGIS extension not available");
}
// Test spatial reference systems
const srsResult = await client.query(
"SELECT COUNT(*) FROM spatial_ref_sys WHERE srid IN (4326, 3857);"
);
if (parseInt(srsResult.rows[0].count) < 2) {
issues.push("Required spatial reference systems missing");
}
// Test performance
const perfStart = Date.now();
await client.query("SELECT 1;");
const queryTime = Date.now() - perfStart;
if (queryTime > 1000) {
issues.push(`Slow database response: ${queryTime}ms`);
}
await client.end();
} catch (error) {
issues.push(`Connection failed: ${error.message}`);
}
return {
success: issues.length === 0,
issues: issues,
};
}
async testSpatialQuery() {
try {
const client = new Client(this.connectionString);
await client.connect();
// Test spatial index usage
const explainResult = await client.query(`
EXPLAIN ANALYZE
SELECT id FROM locations
WHERE ST_DWithin(geom, ST_Point(-122.4194, 37.7749), 1000)
LIMIT 10;
`);
const plan = explainResult.rows
.map((row) => row["QUERY PLAN"])
.join("\n");
if (!plan.includes("Index")) {
console.warn(
"Spatial query not using index - consider adding GIST index"
);
}
await client.end();
return { plan };
} catch (error) {
return { error: error.message };
}
}
}
// API connectivity diagnostics
class APIDiagnostics {
static async testEndpoint(url, options = {}) {
const startTime = Date.now();
try {
const response = await fetch(url, {
timeout: options.timeout || 10000,
...options,
});
const endTime = Date.now();
const responseTime = endTime - startTime;
return {
success: response.ok,
status: response.status,
responseTime: responseTime,
headers: Object.fromEntries(response.headers.entries()),
contentType: response.headers.get("content-type"),
};
} catch (error) {
return {
success: false,
error: error.message,
responseTime: Date.now() - startTime,
};
}
}
static async runConnectivityTest(endpoints) {
const results = {};
for (const [name, url] of Object.entries(endpoints)) {
console.log(`Testing ${name}...`);
results[name] = await this.testEndpoint(url);
if (results[name].success) {
console.log(`✓ ${name}: ${results[name].responseTime}ms`);
} else {
console.log(`✗ ${name}: ${results[name].error}`);
}
}
return results;
}
}
// Usage
const diagnostics = new DatabaseDiagnostics(process.env.DATABASE_URL);
const dbResults = await diagnostics.diagnoseConnection();
const apiResults = await APIDiagnostics.runConnectivityTest({
OpenStreetMap: "https://tile.openstreetmap.org/0/0/0.png",
Nominatim:
"https://nominatim.openstreetmap.org/search?q=test&format=json&limit=1",
Overpass:
"https://overpass-api.de/api/interpreter?data=[out:json];node(0,0,0,0);out;",
});
Deployment Issues#
HTTPS and Security#
// SSL/TLS certificate validation
class SecurityDiagnostics {
static async checkSSL(domain) {
try {
const response = await fetch(`https://${domain}`, {
method: "HEAD",
});
return {
secure: true,
status: response.status,
headers: {
hsts: response.headers.get("strict-transport-security"),
csp: response.headers.get("content-security-policy"),
xframe: response.headers.get("x-frame-options"),
},
};
} catch (error) {
return {
secure: false,
error: error.message,
};
}
}
static checkMixedContent() {
const issues = [];
// Check for mixed content in current page
if (location.protocol === "https:") {
const scripts = document.querySelectorAll("script[src]");
const links = document.querySelectorAll("link[href]");
const images = document.querySelectorAll("img[src]");
[...scripts, ...links, ...images].forEach((element) => {
const url = element.src || element.href;
if (url && url.startsWith("http:")) {
issues.push({
type: element.tagName.toLowerCase(),
url: url,
element: element,
});
}
});
}
return issues;
}
static validateCSP() {
const csp = document.querySelector(
'meta[http-equiv="Content-Security-Policy"]'
);
if (!csp) {
return {
present: false,
recommendation: "Add Content-Security-Policy header",
};
}
const policy = csp.content;
const directives = policy.split(";").map((d) => d.trim());
const recommendations = [];
if (!directives.some((d) => d.startsWith("default-src"))) {
recommendations.push("Add default-src directive");
}
if (!directives.some((d) => d.startsWith("script-src"))) {
recommendations.push("Add script-src directive");
}
return {
present: true,
policy: policy,
recommendations: recommendations,
};
}
}
Environment Configuration#
// Environment validation
class EnvironmentValidator {
static validateEnvironment() {
const issues = [];
const warnings = [];
// Check required environment variables
const required = ["DATABASE_URL", "REDIS_URL", "JWT_SECRET"];
required.forEach((envVar) => {
if (!process.env[envVar]) {
issues.push(`Missing required environment variable: ${envVar}`);
}
});
// Check development vs production settings
if (process.env.NODE_ENV === "production") {
if (process.env.JWT_SECRET === "development-secret") {
issues.push("Using development JWT secret in production");
}
if (process.env.DEBUG === "true") {
warnings.push("Debug mode enabled in production");
}
}
// Check database URL format
if (
process.env.DATABASE_URL &&
!process.env.DATABASE_URL.startsWith("postgresql://")
) {
warnings.push("DATABASE_URL may not be in correct format");
}
return {
valid: issues.length === 0,
issues: issues,
warnings: warnings,
};
}
static checkDependencies() {
const packageJson = require("./package.json");
const issues = [];
// Check for known vulnerable packages
const vulnerablePackages = [
"lodash@4.17.15", // Example - check for specific vulnerable versions
];
Object.entries(packageJson.dependencies || {}).forEach(([pkg, version]) => {
const packageWithVersion = `${pkg}@${version}`;
if (vulnerablePackages.includes(packageWithVersion)) {
issues.push(`Vulnerable package detected: ${packageWithVersion}`);
}
});
return {
total: Object.keys(packageJson.dependencies || {}).length,
vulnerabilities: issues,
};
}
}
Browser Compatibility#
Feature Detection#
class CompatibilityChecker {
static checkBrowserSupport() {
const features = {
webgl: this.hasWebGL(),
geolocation: "geolocation" in navigator,
localStorage: this.hasLocalStorage(),
fetch: "fetch" in window,
promises: "Promise" in window,
webWorkers: "Worker" in window,
serviceWorkers: "serviceWorker" in navigator,
touchEvents: "ontouchstart" in window,
deviceMotion: "DeviceMotionEvent" in window,
};
const unsupported = Object.entries(features)
.filter(([feature, supported]) => !supported)
.map(([feature]) => feature);
return {
features: features,
unsupported: unsupported,
grade: this.getBrowserGrade(features),
};
}
static hasWebGL() {
try {
const canvas = document.createElement("canvas");
return !!(
canvas.getContext("webgl") || canvas.getContext("experimental-webgl")
);
} catch (e) {
return false;
}
}
static hasLocalStorage() {
try {
const test = "localStorage-test";
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
static getBrowserGrade(features) {
const essential = ["webgl", "fetch", "promises"];
const nice = ["localStorage", "webWorkers", "geolocation"];
const essentialSupported = essential.every((f) => features[f]);
const niceSupported = nice.filter((f) => features[f]).length;
if (!essentialSupported) {
return "F"; // Unsupported
} else if (niceSupported >= 2) {
return "A"; // Full support
} else if (niceSupported >= 1) {
return "B"; // Good support
} else {
return "C"; // Basic support
}
}
static getPolyfillRecommendations() {
const support = this.checkBrowserSupport();
const recommendations = [];
if (!support.features.fetch) {
recommendations.push("Include fetch polyfill (whatwg-fetch)");
}
if (!support.features.promises) {
recommendations.push("Include Promise polyfill (es6-promise)");
}
if (!support.features.webgl) {
recommendations.push("Consider fallback to Canvas 2D rendering");
}
return recommendations;
}
}
// Browser-specific workarounds
class BrowserWorkarounds {
static applyIOSFixes() {
// iOS Safari viewport fix
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
const viewport = document.querySelector("meta[name=viewport]");
if (viewport) {
viewport.content =
"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no";
}
// Prevent zoom on input focus
document.addEventListener("touchstart", () => {}, { passive: true });
}
}
static applySafariWorkarounds() {
// Safari-specific fixes
if (
/Safari/.test(navigator.userAgent) &&
!/Chrome/.test(navigator.userAgent)
) {
// Fix for Safari tile loading issues
if (window.map && window.map.getContainer) {
const container = window.map.getContainer();
container.style.transform = "translateZ(0)";
}
}
}
static applyAllWorkarounds() {
this.applyIOSFixes();
this.applySafariWorkarounds();
}
}
// Usage
const compatibility = CompatibilityChecker.checkBrowserSupport();
console.log("Browser compatibility:", compatibility);
if (compatibility.grade === "F") {
console.error("Browser not supported");
// Show fallback interface
} else if (compatibility.unsupported.length > 0) {
console.warn("Some features not supported:", compatibility.unsupported);
const recommendations = CompatibilityChecker.getPolyfillRecommendations();
console.log("Polyfill recommendations:", recommendations);
}
BrowserWorkarounds.applyAllWorkarounds();
This troubleshooting guide provides systematic approaches to identifying and resolving common Web GIS issues. The diagnostic tools and solutions can be adapted to specific application requirements and deployment environments.