using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.IO; namespace GitConverter.Lib.Converters { /// /// Helper that detects the specific JSON GIS format for a given JSON payload or file. /// /// /// - Supported detections: /// * GeoJson : FeatureCollection objects (checks "type":"FeatureCollection") /// * EsriJson : Esri JSON feature set (checks presence of "features" and "spatialReference") /// * GeoJsonSeq : Newline-delimited/sequence GeoJSON (JSON array or NDJSON) /// * TopoJson : TopoJSON (checks "type":"Topology") /// - Implementation notes: /// * The detector does not assume any particular ordering of object properties. /// It scans all top-level properties once and performs early exits when a match is found. /// * NDJSON (newline-delimited JSON) is detected by probing the first non-empty line when /// a full-document parse fails. /// - Methods provide both exception-throwing and non-throwing variants. /// - Detection is heuristic based on top-level JSON structure and keys; for ambiguous inputs consider /// additional content inspection or an explicit conversion option. /// - This helper depends on Newtonsoft.Json (Json.NET). For very large files consider streaming/parsing only the header. /// public static class JsonFormatDetector { /// /// Enumeration of detected JSON GIS formats. /// public enum Format { Unknown = 0, GeoJson, EsriJson, GeoJsonSeq, TopoJson } /// /// Detect format from a JSON file. Throws on IO or parse errors. /// public static Format DetectFromFile(string filePath) { if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException(nameof(filePath)); var text = File.ReadAllText(filePath); return DetectFromString(text); } /// /// Non-throwing variant that attempts to detect format from a JSON file. /// Returns true when detection succeeded (result may be Unknown), false when file/read/parse failed. /// public static bool TryDetectFromFile(string filePath, out Format result) { result = Format.Unknown; try { if (string.IsNullOrWhiteSpace(filePath)) return false; if (!File.Exists(filePath)) return false; var text = File.ReadAllText(filePath); result = DetectFromString(text); return true; } catch { return false; } } /// /// Detect format from a JSON string payload. /// public static Format DetectFromString(string json) { if (string.IsNullOrWhiteSpace(json)) return Format.Unknown; // Try parse as JToken to handle arrays and objects. JToken token; bool ndjsonProbe = false; try { token = JToken.Parse(json); } catch (JsonReaderException) { // Could be NDJSON (newline-delimited JSON). Try to detect by reading first non-empty line. var firstLine = ReadFirstNonEmptyLine(json); if (firstLine != null) { try { token = JToken.Parse(firstLine); // If we successfully parsed the first line of a non-JSON-root document, treat as NDJSON -> GeoJsonSeq. ndjsonProbe = true; } catch { return Format.Unknown; } } else { return Format.Unknown; } } // If we discovered this by probing the first line of NDJSON, consider it a GeoJsonSeq. if (ndjsonProbe) { return Format.GeoJsonSeq; } // GeoJsonSeq: top-level array of GeoJSON objects if (token.Type == JTokenType.Array) { return Format.GeoJsonSeq; } if (token.Type != JTokenType.Object) { return Format.Unknown; } var obj = (JObject)token; // Scan all top-level properties once and exit early on a decisive match. bool hasFeatures = false; bool hasSpatialRef = false; string typeValue = null; foreach (var prop in obj.Properties()) { var name = prop.Name; if (string.Equals(name, "type", StringComparison.OrdinalIgnoreCase)) { typeValue = prop.Value?.ToString(); // Immediate recognition for TopoJSON or GeoJSON FeatureCollection if (!string.IsNullOrEmpty(typeValue)) { if (string.Equals(typeValue, "Topology", StringComparison.OrdinalIgnoreCase)) return Format.TopoJson; if (string.Equals(typeValue, "FeatureCollection", StringComparison.OrdinalIgnoreCase)) return Format.GeoJson; } } else if (string.Equals(name, "features", StringComparison.OrdinalIgnoreCase)) { hasFeatures = true; if (hasSpatialRef) return Format.EsriJson; // both present -> Esri JSON } else if (string.Equals(name, "spatialReference", StringComparison.OrdinalIgnoreCase)) { hasSpatialRef = true; if (hasFeatures) return Format.EsriJson; // both present -> Esri JSON } // continue scanning other properties until a decisive match occurs } // If we saw features but no spatialReference, treat as EsriJson (common variant) if (hasFeatures) return Format.EsriJson; return Format.Unknown; } /// /// Return the first non-empty line from the provided text (used to sniff NDJSON). /// private static string ReadFirstNonEmptyLine(string text) { using (var sr = new StringReader(text)) { string line; while ((line = sr.ReadLine()) != null) { if (!string.IsNullOrWhiteSpace(line)) return line; } } return null; } } }