GeoKMZer

geoKMZer is a JavaScript library designed to convert KMZ into KML files, use with GeoKMLer to convert to GeoJSON.

Ovu skriptu ne treba izravno instalirati. To je biblioteka za druge skripte koje se uključuju u meta direktivu // @require https://update.greatest.deepsurf.us/scripts/527113/1538395/GeoKMZer.js

// ==UserScript==
// @name                GeoKMZer
// @namespace           https://github.com/JS55CT
// @description         geoKMZer is a JavaScript library designed to convert KMZ into KML files, use with GeoKMLer to convert to GeoJSON.
// @version             1.1.0
// @author              JS55CT
// @license             MIT
// @match              *://this-library-is-not-supposed-to-run.com/*
// ==/UserScript==

/***********************************************************
 * ## Project Home < https://github.com/JS55CT/GeoKMLer >
 *  MIT License
 * Copyright (c) 2025 Justin
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 **************************************************************/
var GeoKMZer = (function () {
  /**
   * GeoKMZer constructor function, which optionally wraps an object.
   * @param {Object} [obj] - Optional object to wrap.
   * @returns {GeoKMZer} - An instance of GeoKMZer.
   */
  function GeoKMZer(obj) {
    if (obj instanceof GeoKMZer) return obj;
    if (!(this instanceof GeoKMZer)) return new GeoKMZer(obj);
    this._wrapped = obj; // Optional: wrap any input object if needed
  }

  /**
   * Converts a buffer of various types to a Uint8Array.
   * @param {ArrayBuffer|TypedArray} buffer - The buffer to convert.
   * @returns {Uint8Array} - The converted Uint8Array.
   * @throws Will throw an error if the buffer is not a valid buffer-like object.
   */
  function toUint8Array(buffer) {
    if (!buffer) {
      throw new Error("forgot to pass buffer");
    }
    if (ArrayBuffer.isView(buffer)) {
      // Buffer is a typed array view like Uint8Array
      return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
    }
    if (buffer instanceof ArrayBuffer) {
      // Buffer is an ArrayBuffer
      return new Uint8Array(buffer);
    }
    throw new Error("invalid buffer like object");
  }

  /**
   * Yields entries from a ZIP archive contained in a buffer.
   * @generator
   * @param {Uint8Array} buffer - The buffer representing the ZIP file.
   * @yields {Object} - An object containing filename, comment, and a read() method to get file content.
   */
  GeoKMZer.prototype.parseZipEntries = function* (buffer) {
    const textDecoder = new TextDecoder();

    const decodeText = (buffer) => textDecoder.decode(buffer);

    const findEndOfCentralDirectory = (buffer) => {
      let offset = buffer.length - 20;
      const minSearchOffset = Math.max(offset - 65516, 2);
      while ((offset = buffer.lastIndexOf(80, offset - 1)) !== -1 && !(buffer[offset + 1] === 75 && buffer[offset + 2] === 5 && buffer[offset + 3] === 6) && offset > minSearchOffset);
      return offset;
    };

    const throwError = (message) => {
      throw new Error("unzip-error: " + message);
    };

    // Declare the decompression handling function
    let decompressWithDecompressionStream;
    const compressionFormat = "deflate-raw";

    try {
      new self.DecompressionStream(compressionFormat);
      decompressWithDecompressionStream = async (compressedData) => {
        const decompressionStream = new self.DecompressionStream(compressionFormat);
        const writer = decompressionStream.writable.getWriter();
        const reader = decompressionStream.readable.getReader();

        writer.write(compressedData);
        writer.close();

        const decompressedChunks = [];
        let totalLength = 0;
        let position = 0;
        let readResult;

        while (!(readResult = await reader.read()).done) {
          const chunk = readResult.value;
          decompressedChunks.push(chunk);
          totalLength += chunk.length;
        }

        if (decompressedChunks.length > 1) {
          const combinedArray = new Uint8Array(totalLength);
          for (const chunk of decompressedChunks) {
            combinedArray.set(chunk, position);
            position += chunk.length;
          }
          return combinedArray;
        } else {
          return decompressedChunks[0];
        }
      };
    } catch {
      console.error("DecompressionStream is unsupported or initialization failed.");
    }

    let centralDirectoryEnd = findEndOfCentralDirectory(buffer);

    if (centralDirectoryEnd === -1) {
      throwError(2);
    }

    const subarray = (start, length) => buffer.subarray((centralDirectoryEnd += start), (centralDirectoryEnd += length));
    const dataView = new DataView(buffer.buffer, buffer.byteOffset);
    const getUint16 = (offset) => dataView.getUint16(offset + centralDirectoryEnd, true);
    const getUint32 = (offset) => dataView.getUint32(offset + centralDirectoryEnd, true);

    let numberOfEntries = getUint16(10);

    if (numberOfEntries !== getUint16(8)) {
      throwError(3);
    }

    centralDirectoryEnd = getUint32(16);

    while (numberOfEntries--) {
      let compressionType = getUint16(10),
        filenameLength = getUint16(28),
        extraFieldLength = getUint16(30),
        fileCommentLength = getUint16(32),
        compressedSize = getUint32(20),
        localHeaderOffset = getUint32(42),
        filename = decodeText(subarray(46, filenameLength)),
        comment = decodeText(subarray(extraFieldLength, fileCommentLength)),
        previousCentralDirectoryEnd = centralDirectoryEnd,
        compressedData;

      centralDirectoryEnd = localHeaderOffset;
      compressedData = subarray(30 + getUint16(26) + getUint16(28), compressedSize);

      yield {
        filename,
        comment,
        read: () => {
          if (compressionType & 8) {
            return decompressWithDecompressionStream(compressedData);
          } else if (compressionType) {
            throwError(1);
          } else {
            return compressedData;
          }
        },
      };

      centralDirectoryEnd = previousCentralDirectoryEnd;
    }
  };

  /**
   * Unzips a KMZ buffer, potentially recursively, and retrieves contained KML files.
   * @param {ArrayBuffer|TypedArray} buffer - The buffer of the KMZ file.
   * @param {string} [parentFile=''] - Name of the parent file if dealing with nested KMZ files.
   * @returns {Object} - An object containing file names and their corresponding data buffers.
   * @throws Will throw an error if no KML files are found.
   */
  GeoKMZer.prototype.unzipKMZ = async function (buffer, parentFile = "") {
    const files = {};
    const kmlFileRegex = /.+\.kml$/i;
    const kmzFileRegex = /.+\.kmz$/i;
    const uint8Buffer = toUint8Array(buffer);

    for (const entry of this.parseZipEntries(uint8Buffer)) {
      if (kmlFileRegex.test(entry.filename)) {
        files[entry.filename] = await entry.read();
      } else if (kmzFileRegex.test(entry.filename)) {
        // Handle nested KMZ file
        try {
          const nestedKMZBuffer = await entry.read();
          const nestedFiles = await this.unzipKMZ(nestedKMZBuffer, entry.filename);
          Object.assign(files, nestedFiles); // Merge files found in nested archives
        } catch (nestedError) {
          console.error(`Error reading nested KMZ file "${entry.filename}":`, nestedError);
        }
      } 
    }

    if (Object.keys(files).length === 0) {
      throw new Error("No KML file found in the KMZ archive.");
    }

    return files;
  };

  /**
   * Reads a KMZ buffer and extracts KML files into an array of textual contents.
   * @param {ArrayBuffer|TypedArray} buffer - The buffer of the KMZ file.
   * @returns {Array} - An array of objects, each containing the filename and content of a KML file.
   * @throws Will log errors if any occur during KMZ reading.
   */
  GeoKMZer.prototype.read = async function (buffer) {
    try {
      const kmlFiles = await this.unzipKMZ(buffer);
      const textDecoder = new TextDecoder();
      const kmlContentsArray = [];

      for (const [kmlFilename, kmlBuffer] of Object.entries(kmlFiles)) {
        const kmlContent = textDecoder.decode(kmlBuffer); // Decode the KML buffer to text
        kmlContentsArray.push({ filename: kmlFilename, content: kmlContent }); // Store each content with its filename
      }

      return kmlContentsArray;
    } catch (error) {
      console.error("Error during KMZ reading:", error);
    }
  };

  return GeoKMZer;
})();