external/gridded-data/geospatial/geospatial.js

import * as divisors from "../../../modules/visualize/divisors.js";
import { loamService } from "./loam/loam-use.js";

/**
 * Geospatial data format loader
 * Provides lazy loading for geospatial libraries and dependencies
 * @external GeospatialLoader
 */

let geospatialLibraries = null;
let isGeospatialLoaded = false;


/**
 * Loads geospatial-related libraries and dependencies
 * @method load
 * @memberof GeospatialLoader
 * @param {Object} [options] - Loading options
 * @param {boolean} [options.includeProj4=true] - Include Proj4 coordinate transformations
 * @param {boolean} [options.includeGeoTIFF=true] - Include GeoTIFF support
 * @param {boolean} [options.includeTurf=true] - Include Turf.js for geospatial operations
 * @returns {Promise<Object>} Promise resolving to loaded geospatial libraries
 * @example
 * await geospatialLoader.load({
 *   includeProj4: true,
 *   includeGeoTIFF: true,
 *   includeTurf: true
 * });
 */
async function load(options = {}) {
  const {
    includeProj4 = true,
    includeGeoTIFF = true,
    includeTurf = true,
    includeGeolib = true,
    includeGDAL = true,
  } = options;

  if (isGeospatialLoaded) {
    return geospatialLibraries;
  }

  try {
    console.log('Loading geospatial libraries...');

    let libraries = [];

    if (includeProj4) {
      const proj4 = divisors.createScript({
        params: {
          src: "https://cdn.jsdelivr.net/npm/proj4@2.9.0/dist/proj4.min.js",
          name: "proj4"
        }
      });
      libraries.push(proj4);
    }

    if (includeGeoTIFF) {
      const geotiff = divisors.createScript({
        params: {
          src: "https://cdn.jsdelivr.net/npm/geotiff@2.1.3/dist-browser/geotiff.js",
          name: "geotiff"
        }
      });
      libraries.push(geotiff);

      const pako = divisors.createScript({
        params: {
          src: "https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako.min.js",
          name: "pako"
        }
      });
      libraries.push(pako);
    }

    if (includeTurf) {
      const turf = divisors.createScript({
        params: {
          src: "https://cdn.jsdelivr.net/npm/@turf/turf@6.5.0/turf.min.js",
          name: "turf"
        }
      });
      libraries.push(turf);
    }

    if (includeGeolib) {
      const geoUtils = divisors.createScript({
        params: {
          src: "https://cdn.jsdelivr.net/npm/geolib@3.3.3/lib/index.min.js",
          name: "geolib"
        }
      });
      libraries.push(geoUtils);
    }

    /* 
     * Load Loam (GDAL WASM) via LoamService
     */
    let loamInstance = null;
    if (includeGDAL) {
      loamInstance = loamService; // Use the imported singleton service
    }

    // Wait for all scripts to load
    await Promise.all(
      libraries.map(lib => new Promise((resolve, reject) => {
        lib.onload = resolve;
        lib.onerror = () => reject(new Error(`Failed to load library: ${lib.src}`));
      }))
    );

    // Create library interface
    const globalObj = typeof window !== 'undefined' ? window : globalThis;
    geospatialLibraries = {
      proj4: globalObj.proj4 || null,
      GeoTIFF: globalObj.GeoTIFF || null,
      pako: globalObj.pako || null,
      turf: globalObj.turf || null,
      geolib: globalObj.geolib || null,
      loam: loamInstance || null, // Use our instance
      loadedAt: new Date()
    };

    // Initialize loam (GDAL) if loaded
    if (geospatialLibraries.loam) {
      try {
        await geospatialLibraries.loam.initialize();
        console.log('GDAL (loam) initialized via LoamService');
      } catch (e) {
        console.warn('GDAL (loam) initialization failed:', e);
      }
    }

    // Define common projections if proj4 is available
    if (geospatialLibraries.proj4) {
      geospatialLibraries.proj4.defs("EPSG:5070", "+proj=aea +lat_1=29.5 +lat_2=45.5 +lat_0=23 +lon_0=-96 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs");
      geospatialLibraries.proj4.defs("EPSG:4269", "+proj=longlat +ellps=GRS80 +datum=NAD83 +no_defs");
      console.log('Defined common Proj4 projections (EPSG:5070, EPSG:4269)');
    }

    isGeospatialLoaded = true;
    console.log('Geospatial libraries loaded successfully');

    return geospatialLibraries;

  } catch (error) {
    console.error('Failed to load geospatial libraries:', error);
    throw new Error(`Geospatial library loading failed: ${error.message}`);
  }
}

/**
 * Checks if geospatial libraries are loaded
 * @method isLoaded
 * @memberof GeospatialLoader
 * @returns {boolean} True if libraries are loaded
 * @example
 * geospatialLoader.isLoaded();
 */
function isLoaded() {
  return isGeospatialLoaded && geospatialLibraries !== null;
}

/**
 * Gets information about the geospatial loader
 * @method getInfo
 * @memberof GeospatialLoader
 * @returns {Object} Information about supported features
 * @example
 * geospatialLoader.getInfo();
 */
function getInfo() {
  return {
    name: 'Geospatial',
    description: 'Geospatial data processing and coordinate systems',
    features: [
      'Coordinate transformations',
      'GeoTIFF support',
      'Generic TIFF support (fallback)',
      'Gzip decompression',
      'Geospatial operations',
      'Distance calculations',
      'Geometry processing'
    ],
    dependencies: [
      'proj4 (Coordinate transformations)',
      'geotiff (GeoTIFF support)',
      'tiff (Generic TIFF support)',
      'pako (Gzip decompression)',
      'turf (Geospatial operations)',
      'geolib (Geospatial utilities)'
    ]
  };
}

/**
 * Transforms coordinates between different coordinate systems
 * @method transformCoordinates
 * @memberof GeospatialLoader
 * @param {Array} coordinates - [longitude, latitude] coordinates
 * @param {string} fromProj - Source projection (e.g., 'EPSG:4326')
 * @param {string} toProj - Target projection
 * @returns {Array} Transformed coordinates
 * @example
 * const transformed = geospatialLoader.transformCoordinates(
 *   [-74.0060, 40.7128],
 *   'EPSG:4326',
 *   'EPSG:3857'
 * );
 */
function transformCoordinates(coordinates, fromProj, toProj) {
  if (!isLoaded() || !geospatialLibraries.proj4) {
    throw new Error('Geospatial libraries not loaded');
  }

  try {
    return geospatialLibraries.proj4(fromProj, toProj, coordinates);
  } catch (error) {
    throw new Error(`Coordinate transformation failed: ${error.message}`);
  }
}

/**
 * Calculates distance between two points
 * @method calculateDistance
 * @memberof GeospatialLoader
 * @param {Object} point1 - First point {latitude, longitude}
 * @param {Object} point2 - Second point {latitude, longitude}
 * @param {string} [unit='meters'] - Distance unit
 * @returns {number} Distance between points
 * @example
 * const distance = geospatialLoader.calculateDistance(
 *   { latitude: 40.7128, longitude: -74.0060 },
 *   { latitude: 34.0522, longitude: -118.2437 },
 *   'miles'
 * );
 */
function calculateDistance(point1, point2, unit = 'meters') {
  if (!isLoaded() || !geospatialLibraries.geolib) {
    throw new Error('Geospatial libraries not loaded');
  }

  try {
    return geospatialLibraries.geolib.getDistance(point1, point2, unit);
  } catch (error) {
    throw new Error(`Distance calculation failed: ${error.message}`);
  }
}

/**
 * Decompresses gzip data
 * @param {ArrayBuffer} compressedData - Gzipped data
 * @returns {Promise<ArrayBuffer>} Decompressed data
 */
async function decompressGzip(compressedData) {
  // Use browser's built-in decompression if available
  if (typeof DecompressionStream !== 'undefined') {
    const decompressedStream = new Response(compressedData).body
      .pipeThrough(new DecompressionStream('gzip'));
    return new Response(decompressedStream).arrayBuffer();
  }

  // Fallback: try to use pako if available
  if (typeof globalThis.pako !== 'undefined') {
    return globalThis.pako.ungzip(compressedData);
  } else if (typeof window !== 'undefined' && window.pako) {
    return window.pako.ungzip(compressedData);
  }

  throw new Error('No gzip decompression available');
}

/**
 * Loads GeoTIFF from array buffer directly
 * @param {ArrayBuffer} arrayBuffer - Raw TIFF data
 * @returns {Promise<Object>} Parsed TIFF data
 */
async function loadGeoTIFFFromBuffer(arrayBuffer) {
  // Try GeoTIFF first
  if (geospatialLibraries.GeoTIFF) {
    try {
      const geotiff = await geospatialLibraries.GeoTIFF.fromArrayBuffer(arrayBuffer);
      return { type: 'geotiff', data: geotiff };
    } catch (geotiffError) {
      console.warn('GeoTIFF buffer parsing failed:', geotiffError.message);
    }
  }

  // Fallback to Tiff.js
  if (geospatialLibraries.Tiff) {
    try {
      const tiff = new geospatialLibraries.Tiff({ buffer: arrayBuffer });
      return { type: 'tiff', data: tiff };
    } catch (tiffError) {
      console.warn('Tiff.js buffer parsing failed:', tiffError.message);
    }
  }

  throw new Error('No TIFF parser could handle the buffer');
}

/**
 * Loads a GeoTIFF file from URL with fallback to Tiff.js
 * @method loadGeoTIFF
 * @memberof GeospatialLoader
 * @param {string} url - URL to GeoTIFF file
 * @returns {Promise} Promise resolving to GeoTIFF/TIFF object
 * @example
 * const geotiff = await geospatialLoader.loadGeoTIFF('https://example.com/data.tif');
 */
async function loadGeoTIFF(url) {
  if (!isLoaded()) {
    throw new Error('Geospatial libraries not loaded');
  }

  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const arrayBuffer = await response.arrayBuffer();

    // Debug: Check file signature
    const uint8Array = new Uint8Array(arrayBuffer);
    const signature = Array.from(uint8Array.slice(0, 16)).map(b => b.toString(16).padStart(2, '0')).join(' ');
    console.log('File signature (first 16 bytes):', signature);

    // Check for common signatures
    if (signature.startsWith('49 49') || signature.startsWith('4d 4d')) {
      console.log('File appears to be a standard TIFF');
    } else if (signature.startsWith('47 52 49 42')) {
      console.log('File appears to be a GRIB file (not TIFF)');
      throw new Error('File is actually a GRIB file, not TIFF');
    } else if (signature.startsWith('1f 8b')) {
      console.log('File is still gzipped, decompressing manually...');

      // Try manual decompression first (more reliable for MRMS files)
      try {
        console.log('Attempting manual gzip decompression...');
        // Use pako for more reliable decompression
        const pakoLib = globalThis.pako || (typeof window !== 'undefined' && window.pako);
        if (pakoLib) {
          const decompressed = pakoLib.ungzip(new Uint8Array(arrayBuffer));
          console.log(`Decompressed from ${arrayBuffer.byteLength} to ${decompressed.length} bytes`);

          // Convert back to ArrayBuffer for GeoTIFF
          const decompressedBuffer = decompressed.buffer.slice(
            decompressed.byteOffset,
            decompressed.byteOffset + decompressed.byteLength
          );

          return loadGeoTIFFFromBuffer(decompressedBuffer);
        } else {
          throw new Error('Pako library not available for decompression');
        }
      } catch (manualError) {
        console.warn('Manual decompression failed:', manualError.message);

        // Fallback to built-in decompression
        try {
          console.log('Trying built-in decompression...');
          const decompressed = await decompressGzip(arrayBuffer);
          return loadGeoTIFFFromBuffer(decompressed);
        } catch (builtinError) {
          console.warn('Built-in decompression also failed:', builtinError.message);
        }
      }
    } else {
      console.log('Unknown file signature, might be a proprietary format');
      console.log('Full signature:', signature);
    }

    // Try GeoTIFF first
    if (geospatialLibraries.GeoTIFF) {
      try {
        const geotiff = await geospatialLibraries.GeoTIFF.fromArrayBuffer(arrayBuffer);
        return { type: 'geotiff', data: geotiff };
      } catch (geotiffError) {
        console.warn('GeoTIFF parsing failed:', geotiffError.message);
      }
    }

    // Fallback to Tiff.js
    if (geospatialLibraries.Tiff) {
      try {
        const tiff = new geospatialLibraries.Tiff({ buffer: arrayBuffer });
        return { type: 'tiff', data: tiff };
      } catch (tiffError) {
        console.warn('Tiff.js parsing failed:', tiffError.message);
      }
    }

    // Last resort: try with raw buffer if it's still compressed
    const pakoLib = globalThis.pako || (typeof window !== 'undefined' && window.pako);
    if (pakoLib) {
      try {
        console.log('Trying pako decompression...');
        const decompressed = pakoLib.ungzip(arrayBuffer);
        return await loadGeoTIFFFromBuffer(decompressed);
      } catch (pakoError) {
        console.warn('Pako decompression failed:', pakoError.message);
      }
    }

    throw new Error('No TIFF parser could handle the file');

  } catch (error) {
    throw new Error(`Failed to load TIFF file: ${error.message}`);
  }
}

/**
 * Performs geospatial operations using Turf.js
 * @method geospatialOperation
 * @memberof GeospatialLoader
 * @param {string} operation - Turf.js operation name
 * @param {...*} args - Arguments for the operation
 * @returns {*} Result of the geospatial operation
 * @example
 * const buffer = geospatialLoader.geospatialOperation(
 *   'buffer',
 *   point,
 *   1000,
 *   { units: 'meters' }
 * );
 */
function geospatialOperation(operation, ...args) {
  if (!isLoaded() || !geospatialLibraries.turf) {
    throw new Error('Geospatial libraries not loaded');
  }

  try {
    if (typeof geospatialLibraries.turf[operation] === 'function') {
      return geospatialLibraries.turf[operation](...args);
    } else {
      throw new Error(`Unknown geospatial operation: ${operation}`);
    }
  } catch (error) {
    throw new Error(`Geospatial operation failed: ${error.message}`);
  }
}

export default {
  load,
  isLoaded,
  getInfo,
  transformCoordinates,
  calculateDistance,
  loadGeoTIFF,
  loadGeoTIFFFromBuffer,
  geospatialOperation,
  get libraries() { return geospatialLibraries; }
};