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.