GeoKMZer

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

Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greatest.deepsurf.us/scripts/527113/1538395/GeoKMZer.js

  1. // ==UserScript==
  2. // @name GeoKMZer
  3. // @namespace https://github.com/JS55CT
  4. // @description geoKMZer is a JavaScript library designed to convert KMZ into KML files, use with GeoKMLer to convert to GeoJSON.
  5. // @version 1.1.0
  6. // @author JS55CT
  7. // @license MIT
  8. // @match *://this-library-is-not-supposed-to-run.com/*
  9. // ==/UserScript==
  10.  
  11. /***********************************************************
  12. * ## Project Home < https://github.com/JS55CT/GeoKMLer >
  13. * MIT License
  14. * Copyright (c) 2025 Justin
  15. * Permission is hereby granted, free of charge, to any person obtaining a copy
  16. * of this software and associated documentation files (the "Software"), to deal
  17. * in the Software without restriction, including without limitation the rights
  18. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  19. * copies of the Software, and to permit persons to whom the Software is
  20. * furnished to do so, subject to the following conditions:
  21. *
  22. * The above copyright notice and this permission notice shall be included in all
  23. * copies or substantial portions of the Software.
  24. *
  25. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  26. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  27. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  28. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  29. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  30. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  31. * SOFTWARE.
  32. **************************************************************/
  33. var GeoKMZer = (function () {
  34. /**
  35. * GeoKMZer constructor function, which optionally wraps an object.
  36. * @param {Object} [obj] - Optional object to wrap.
  37. * @returns {GeoKMZer} - An instance of GeoKMZer.
  38. */
  39. function GeoKMZer(obj) {
  40. if (obj instanceof GeoKMZer) return obj;
  41. if (!(this instanceof GeoKMZer)) return new GeoKMZer(obj);
  42. this._wrapped = obj; // Optional: wrap any input object if needed
  43. }
  44.  
  45. /**
  46. * Converts a buffer of various types to a Uint8Array.
  47. * @param {ArrayBuffer|TypedArray} buffer - The buffer to convert.
  48. * @returns {Uint8Array} - The converted Uint8Array.
  49. * @throws Will throw an error if the buffer is not a valid buffer-like object.
  50. */
  51. function toUint8Array(buffer) {
  52. if (!buffer) {
  53. throw new Error("forgot to pass buffer");
  54. }
  55. if (ArrayBuffer.isView(buffer)) {
  56. // Buffer is a typed array view like Uint8Array
  57. return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
  58. }
  59. if (buffer instanceof ArrayBuffer) {
  60. // Buffer is an ArrayBuffer
  61. return new Uint8Array(buffer);
  62. }
  63. throw new Error("invalid buffer like object");
  64. }
  65.  
  66. /**
  67. * Yields entries from a ZIP archive contained in a buffer.
  68. * @generator
  69. * @param {Uint8Array} buffer - The buffer representing the ZIP file.
  70. * @yields {Object} - An object containing filename, comment, and a read() method to get file content.
  71. */
  72. GeoKMZer.prototype.parseZipEntries = function* (buffer) {
  73. const textDecoder = new TextDecoder();
  74.  
  75. const decodeText = (buffer) => textDecoder.decode(buffer);
  76.  
  77. const findEndOfCentralDirectory = (buffer) => {
  78. let offset = buffer.length - 20;
  79. const minSearchOffset = Math.max(offset - 65516, 2);
  80. while ((offset = buffer.lastIndexOf(80, offset - 1)) !== -1 && !(buffer[offset + 1] === 75 && buffer[offset + 2] === 5 && buffer[offset + 3] === 6) && offset > minSearchOffset);
  81. return offset;
  82. };
  83.  
  84. const throwError = (message) => {
  85. throw new Error("unzip-error: " + message);
  86. };
  87.  
  88. // Declare the decompression handling function
  89. let decompressWithDecompressionStream;
  90. const compressionFormat = "deflate-raw";
  91.  
  92. try {
  93. new self.DecompressionStream(compressionFormat);
  94. decompressWithDecompressionStream = async (compressedData) => {
  95. const decompressionStream = new self.DecompressionStream(compressionFormat);
  96. const writer = decompressionStream.writable.getWriter();
  97. const reader = decompressionStream.readable.getReader();
  98.  
  99. writer.write(compressedData);
  100. writer.close();
  101.  
  102. const decompressedChunks = [];
  103. let totalLength = 0;
  104. let position = 0;
  105. let readResult;
  106.  
  107. while (!(readResult = await reader.read()).done) {
  108. const chunk = readResult.value;
  109. decompressedChunks.push(chunk);
  110. totalLength += chunk.length;
  111. }
  112.  
  113. if (decompressedChunks.length > 1) {
  114. const combinedArray = new Uint8Array(totalLength);
  115. for (const chunk of decompressedChunks) {
  116. combinedArray.set(chunk, position);
  117. position += chunk.length;
  118. }
  119. return combinedArray;
  120. } else {
  121. return decompressedChunks[0];
  122. }
  123. };
  124. } catch {
  125. console.error("DecompressionStream is unsupported or initialization failed.");
  126. }
  127.  
  128. let centralDirectoryEnd = findEndOfCentralDirectory(buffer);
  129.  
  130. if (centralDirectoryEnd === -1) {
  131. throwError(2);
  132. }
  133.  
  134. const subarray = (start, length) => buffer.subarray((centralDirectoryEnd += start), (centralDirectoryEnd += length));
  135. const dataView = new DataView(buffer.buffer, buffer.byteOffset);
  136. const getUint16 = (offset) => dataView.getUint16(offset + centralDirectoryEnd, true);
  137. const getUint32 = (offset) => dataView.getUint32(offset + centralDirectoryEnd, true);
  138.  
  139. let numberOfEntries = getUint16(10);
  140.  
  141. if (numberOfEntries !== getUint16(8)) {
  142. throwError(3);
  143. }
  144.  
  145. centralDirectoryEnd = getUint32(16);
  146.  
  147. while (numberOfEntries--) {
  148. let compressionType = getUint16(10),
  149. filenameLength = getUint16(28),
  150. extraFieldLength = getUint16(30),
  151. fileCommentLength = getUint16(32),
  152. compressedSize = getUint32(20),
  153. localHeaderOffset = getUint32(42),
  154. filename = decodeText(subarray(46, filenameLength)),
  155. comment = decodeText(subarray(extraFieldLength, fileCommentLength)),
  156. previousCentralDirectoryEnd = centralDirectoryEnd,
  157. compressedData;
  158.  
  159. centralDirectoryEnd = localHeaderOffset;
  160. compressedData = subarray(30 + getUint16(26) + getUint16(28), compressedSize);
  161.  
  162. yield {
  163. filename,
  164. comment,
  165. read: () => {
  166. if (compressionType & 8) {
  167. return decompressWithDecompressionStream(compressedData);
  168. } else if (compressionType) {
  169. throwError(1);
  170. } else {
  171. return compressedData;
  172. }
  173. },
  174. };
  175.  
  176. centralDirectoryEnd = previousCentralDirectoryEnd;
  177. }
  178. };
  179.  
  180. /**
  181. * Unzips a KMZ buffer, potentially recursively, and retrieves contained KML files.
  182. * @param {ArrayBuffer|TypedArray} buffer - The buffer of the KMZ file.
  183. * @param {string} [parentFile=''] - Name of the parent file if dealing with nested KMZ files.
  184. * @returns {Object} - An object containing file names and their corresponding data buffers.
  185. * @throws Will throw an error if no KML files are found.
  186. */
  187. GeoKMZer.prototype.unzipKMZ = async function (buffer, parentFile = "") {
  188. const files = {};
  189. const kmlFileRegex = /.+\.kml$/i;
  190. const kmzFileRegex = /.+\.kmz$/i;
  191. const uint8Buffer = toUint8Array(buffer);
  192.  
  193. for (const entry of this.parseZipEntries(uint8Buffer)) {
  194. if (kmlFileRegex.test(entry.filename)) {
  195. files[entry.filename] = await entry.read();
  196. } else if (kmzFileRegex.test(entry.filename)) {
  197. // Handle nested KMZ file
  198. try {
  199. const nestedKMZBuffer = await entry.read();
  200. const nestedFiles = await this.unzipKMZ(nestedKMZBuffer, entry.filename);
  201. Object.assign(files, nestedFiles); // Merge files found in nested archives
  202. } catch (nestedError) {
  203. console.error(`Error reading nested KMZ file "${entry.filename}":`, nestedError);
  204. }
  205. }
  206. }
  207.  
  208. if (Object.keys(files).length === 0) {
  209. throw new Error("No KML file found in the KMZ archive.");
  210. }
  211.  
  212. return files;
  213. };
  214.  
  215. /**
  216. * Reads a KMZ buffer and extracts KML files into an array of textual contents.
  217. * @param {ArrayBuffer|TypedArray} buffer - The buffer of the KMZ file.
  218. * @returns {Array} - An array of objects, each containing the filename and content of a KML file.
  219. * @throws Will log errors if any occur during KMZ reading.
  220. */
  221. GeoKMZer.prototype.read = async function (buffer) {
  222. try {
  223. const kmlFiles = await this.unzipKMZ(buffer);
  224. const textDecoder = new TextDecoder();
  225. const kmlContentsArray = [];
  226.  
  227. for (const [kmlFilename, kmlBuffer] of Object.entries(kmlFiles)) {
  228. const kmlContent = textDecoder.decode(kmlBuffer); // Decode the KML buffer to text
  229. kmlContentsArray.push({ filename: kmlFilename, content: kmlContent }); // Store each content with its filename
  230. }
  231.  
  232. return kmlContentsArray;
  233. } catch (error) {
  234. console.error("Error during KMZ reading:", error);
  235. }
  236. };
  237.  
  238. return GeoKMZer;
  239. })();