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
JavaScript
;
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;