diff --git a/modules/rmaps/components/map-import.ts b/modules/rmaps/components/map-import.ts new file mode 100644 index 0000000..e6d08b2 --- /dev/null +++ b/modules/rmaps/components/map-import.ts @@ -0,0 +1,84 @@ +/** + * Google Maps GeoJSON parser for rMaps place import. + * Ported from rmaps-online/src/lib/googleMapsParser.ts (pure TS, no React deps). + */ + +export interface ParsedPlace { + name: string; + lat: number; + lng: number; + address?: string; + category?: string; +} + +export interface ParseResult { + success: boolean; + places: ParsedPlace[]; + error?: string; + totalFeatures: number; +} + +const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB + +/** + * Parse a Google Maps GeoJSON export into a list of places. + * Accepts standard GeoJSON FeatureCollection with Point features. + */ +export function parseGoogleMapsGeoJSON(jsonStr: string): ParseResult { + if (jsonStr.length > MAX_FILE_SIZE) { + return { success: false, places: [], error: "File too large (max 50 MB)", totalFeatures: 0 }; + } + + let data: any; + try { + data = JSON.parse(jsonStr); + } catch { + return { success: false, places: [], error: "Invalid JSON", totalFeatures: 0 }; + } + + if (!data || data.type !== "FeatureCollection" || !Array.isArray(data.features)) { + return { success: false, places: [], error: "Not a valid GeoJSON FeatureCollection", totalFeatures: 0 }; + } + + const places: ParsedPlace[] = []; + + for (const feature of data.features) { + if (!feature?.geometry || feature.geometry.type !== "Point") continue; + + const coords = feature.geometry.coordinates; + if (!Array.isArray(coords) || coords.length < 2) continue; + + const lng = coords[0]; + const lat = coords[1]; + + // Validate coordinates + if (typeof lat !== "number" || typeof lng !== "number") continue; + if (lat < -90 || lat > 90 || lng < -180 || lng > 180) continue; + if (isNaN(lat) || isNaN(lng)) continue; + + // Extract name from various properties formats + const props = feature.properties || {}; + const name = + props.name || + props.Name || + props.title || + props.Title || + props["Google Maps URL"]?.split("/place/")?.pop()?.split("/")?.[0]?.replace(/\+/g, " ") || + `Place at ${lat.toFixed(4)}, ${lng.toFixed(4)}`; + + places.push({ + name: String(name).substring(0, 200), + lat, + lng, + address: props.address || props.Address || props.description || undefined, + category: props.category || props.Category || undefined, + }); + } + + return { + success: places.length > 0, + places, + totalFeatures: data.features.length, + ...(places.length === 0 ? { error: "No valid Point features found" } : {}), + }; +}