UNPKG

geo-toolkits

Version:

A comprehensive TypeScript library for geospatial operations including geofencing, polygon containment, area calculations, and coordinate processing. Supports GeoJSON, KML, and KMZ file formats with both local file and remote URL loading capabilities.

337 lines (336 loc) 12.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FormatDetector = void 0; const errors_1 = require("../utils/errors"); class FormatDetector { /** * Detect file format from extension and content with confidence levels */ static detectFormat(source, content) { const results = this.detectFormatWithDetails(source, content); return results.format; } /** * Detect format with detailed information about the detection process */ static detectFormatWithDetails(source, content) { const extension = this.getExtension(source); // First try extension-based detection (highest confidence) const extensionResult = this.detectFromExtension(extension); if (extensionResult) { // Verify extension matches content for high confidence if (this.verifyFormatMatchesContent(extensionResult, content)) { return { format: extensionResult, confidence: "high", detectionMethod: "extension", details: `File extension .${extension} matches content structure`, }; } } // Fall back to content-based detection const contentResult = this.detectFromContent(content); return { format: contentResult.format, confidence: contentResult.confidence, detectionMethod: contentResult.detectionMethod, details: contentResult.details, }; } /** * Detect format from file extension */ static detectFromExtension(extension) { const normalizedExt = extension.toLowerCase(); switch (normalizedExt) { case "geojson": return "geojson"; case "json": return "geojson"; case "kml": return "kml"; case "kmz": return "kmz"; default: return null; } } /** * Detect format from file content with detailed analysis */ static detectFromContent(content) { // Check for ZIP signature first (KMZ files are ZIP archives) if (this.isZipFile(content)) { return { format: "kmz", confidence: "high", detectionMethod: "magic_bytes", details: "ZIP file signature (PK) detected - likely KMZ format", }; } // Get text content for analysis (first 2KB should be enough) const textContent = this.getTextContent(content); // Check for XML content (KML) const xmlAnalysis = this.analyzeXmlContent(textContent); if (xmlAnalysis.isXml) { if (xmlAnalysis.isKml) { return { format: "kml", confidence: "high", detectionMethod: "content", details: "XML content with KML namespace detected", }; } else { return { format: "kml", confidence: "medium", detectionMethod: "content", details: "XML content detected, assuming KML format", }; } } // Check for JSON content (GeoJSON) const jsonAnalysis = this.analyzeJsonContent(textContent); if (jsonAnalysis.isJson) { if (jsonAnalysis.isGeoJson) { return { format: "geojson", confidence: "high", detectionMethod: "content", details: `GeoJSON FeatureCollection with ${jsonAnalysis.featureCount} features detected`, }; } else { return { format: "geojson", confidence: "low", detectionMethod: "content", details: "JSON content detected, assuming GeoJSON format", }; } } // If we can't determine the format, throw an error throw new errors_1.FileLoadError(this.buildUnsupportedFormatError(content), "UNSUPPORTED_FORMAT"); } /** * Check if content is a ZIP file (for KMZ detection) */ static isZipFile(content) { if (content.length < 4) return false; // ZIP files start with PK signature (0x504B) // Check for different ZIP signatures const zipSignatures = [ [0x50, 0x4b, 0x03, 0x04], // Standard ZIP [0x50, 0x4b, 0x05, 0x06], // Empty ZIP [0x50, 0x4b, 0x07, 0x08], // Spanned ZIP ]; return zipSignatures.some((signature) => signature.every((byte, index) => content[index] === byte)); } /** * Get text content from buffer, handling different encodings */ static getTextContent(content) { const sampleSize = Math.min(2048, content.length); const sample = content.subarray(0, sampleSize); try { // Try UTF-8 first const utf8Text = sample.toString("utf8"); // Check if UTF-8 decoding was successful (no replacement characters) if (!utf8Text.includes("\uFFFD")) { return utf8Text; } // Fall back to latin1 if UTF-8 fails return sample.toString("latin1"); } catch (_a) { // Last resort: ASCII return sample.toString("ascii"); } } /** * Analyze XML content structure */ static analyzeXmlContent(content) { const trimmed = content.trim(); // Check for XML declaration or root element const xmlPattern = /^\s*<\?xml|^\s*<[a-zA-Z]/; const isXml = xmlPattern.test(trimmed); if (!isXml) { return { isXml: false, isKml: false }; } // Check for KML-specific patterns const kmlPatterns = [ /<kml\s/i, /xmlns.*opengis\.net\/kml/i, /<Placemark/i, /<Document/i, /<Folder/i, ]; const isKml = kmlPatterns.some((pattern) => pattern.test(trimmed)); return { isXml: true, isKml, details: isKml ? "KML elements detected" : "Generic XML content", }; } /** * Analyze JSON content structure */ static analyzeJsonContent(content) { var _a; const trimmed = content.trim(); // Quick check for JSON structure const jsonPattern = /^\s*[\{\[]/; if (!jsonPattern.test(trimmed)) { return { isJson: false, isGeoJson: false }; } try { const parsed = JSON.parse(trimmed); if (!parsed || typeof parsed !== "object") { return { isJson: true, isGeoJson: false }; } // Check for GeoJSON structure const isGeoJson = this.isGeoJsonObject(parsed); const featureCount = (_a = parsed.features) === null || _a === void 0 ? void 0 : _a.length; return { isJson: true, isGeoJson, featureCount, details: isGeoJson ? `GeoJSON ${parsed.type} with ${featureCount || 0} features` : "Generic JSON object", }; } catch (_b) { // Might be partial JSON or malformed return { isJson: true, isGeoJson: false, details: "Malformed or partial JSON content", }; } } /** * Check if parsed object is valid GeoJSON */ static isGeoJsonObject(obj) { if (!obj || typeof obj !== "object") { return false; } // Check for GeoJSON type field if (obj.type) { const validTypes = [ "FeatureCollection", "Feature", "Point", "LineString", "Polygon", "MultiPoint", "MultiLineString", "MultiPolygon", "GeometryCollection", ]; if (validTypes.includes(obj.type)) { // Additional validation for FeatureCollection if (obj.type === "FeatureCollection") { return Array.isArray(obj.features); } return true; } } return false; } /** * Verify that detected format matches actual content */ static verifyFormatMatchesContent(format, content) { try { switch (format) { case "geojson": const textContent = this.getTextContent(content); const jsonAnalysis = this.analyzeJsonContent(textContent); return jsonAnalysis.isJson; case "kml": const xmlContent = this.getTextContent(content); const xmlAnalysis = this.analyzeXmlContent(xmlContent); return xmlAnalysis.isXml; case "kmz": return this.isZipFile(content); default: return false; } } catch (_a) { return false; } } /** * Extract file extension from source */ static getExtension(source) { // Handle URLs if (source.startsWith("http://") || source.startsWith("https://")) { try { const url = new URL(source); const pathname = url.pathname; // Remove query parameters and extract extension const pathWithoutQuery = pathname.split("?")[0]; const segments = pathWithoutQuery.split("."); return segments.length > 1 ? segments.pop() || "" : ""; } catch (_a) { return ""; } } // Handle file paths const segments = source.split("."); return segments.length > 1 ? segments.pop() || "" : ""; } /** * Build detailed error message for unsupported formats */ static buildUnsupportedFormatError(content) { const textSample = this.getTextContent(content).substring(0, 100); const size = content.length; let errorMsg = `Unable to determine file format. Supported formats: GeoJSON (.geojson, .json), KML (.kml), KMZ (.kmz)\n\n`; errorMsg += `File analysis:\n`; errorMsg += `- Size: ${size} bytes\n`; errorMsg += `- Content preview: "${textSample}${textSample.length >= 100 ? "..." : ""}"\n`; // Provide suggestions based on content if (this.isZipFile(content)) { errorMsg += `- Appears to be a ZIP archive, but may not be a valid KMZ file\n`; } else if (textSample.includes("<")) { errorMsg += `- Contains XML-like content, but doesn't appear to be valid KML\n`; } else if (textSample.includes("{") || textSample.includes("[")) { errorMsg += `- Contains JSON-like content, but doesn't appear to be valid GeoJSON\n`; } return errorMsg; } /** * Get MIME type for detected format */ static getMimeType(format) { const mimeTypes = { geojson: "application/geo+json", kml: "application/vnd.google-earth.kml+xml", kmz: "application/vnd.google-earth.kmz", }; return mimeTypes[format] || "application/octet-stream"; } /** * Get human-readable format description */ static getFormatDescription(format) { const descriptions = { geojson: "GeoJSON (Geographic JavaScript Object Notation)", kml: "KML (Keyhole Markup Language)", kmz: "KMZ (Compressed KML Archive)", }; return descriptions[format] || "Unknown Format"; } } exports.FormatDetector = FormatDetector;