@dunguel/expo-geo-parser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.js ADDED
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['universe/native', 'universe/web'],
4
+ ignorePatterns: ['build'],
5
+ };
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # expo-geo-parser
2
+
3
+ A high-performance Expo native module for parsing geospatial files like KML, KMZ, and other formats, extracting polygons, markers, and spatial data efficiently on iOS and Android.
4
+
5
+ # API documentation
6
+
7
+ - [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/geo-parser/)
8
+ - [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/geo-parser/)
9
+
10
+ # Installation in managed Expo projects
11
+
12
+ For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
13
+
14
+ # Installation in bare React Native projects
15
+
16
+ For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
17
+
18
+ ### Add the package to your npm dependencies
19
+
20
+ ```
21
+ npm install expo-geo-parser
22
+ ```
23
+
24
+ ### Configure for Android
25
+
26
+
27
+
28
+
29
+ ### Configure for iOS
30
+
31
+ Run `npx pod-install` after installing the npm package.
32
+
33
+ # Contributing
34
+
35
+ Contributions are very welcome! Please refer to guidelines described in the [contributing guide]( https://github.com/expo/expo#contributing).
@@ -0,0 +1,18 @@
1
+ plugins {
2
+ id 'com.android.library'
3
+ id 'kotlin-android'
4
+ id 'expo-module-gradle-plugin'
5
+ }
6
+
7
+ group = 'expo.modules.geoparser'
8
+ version = '0.1.0'
9
+
10
+ android {
11
+ namespace "expo.modules.geoparser"
12
+ defaultConfig {
13
+ versionCode 1
14
+ versionName "0.1.0"
15
+ }
16
+ }
17
+
18
+ dependencies {}
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,134 @@
1
+ package expo.modules.geoparser
2
+
3
+ import android.net.Uri
4
+ import expo.modules.geoparser.parsers.GeoJSONParser
5
+ import expo.modules.geoparser.parsers.KMLParser
6
+ import expo.modules.kotlin.modules.Module
7
+ import expo.modules.kotlin.modules.ModuleDefinition
8
+ import java.io.File
9
+ import java.io.InputStream
10
+ import java.util.UUID
11
+ import java.util.zip.ZipInputStream
12
+
13
+ class ExpoGeoParserModule : Module() {
14
+ override fun definition() = ModuleDefinition {
15
+ Name("ExpoGeoParser")
16
+ Events("onParseFeatures")
17
+
18
+ Function("detectFileType") { uri: String ->
19
+ detectFileType(uri)
20
+ }
21
+
22
+ AsyncFunction("parseFile") { uri: String ->
23
+ parseGeoFile(uri) { batch, isLast ->
24
+ sendEvent("onParseFeatures", mapOf(
25
+ "features" to batch,
26
+ "isLast" to isLast
27
+ ))
28
+ }
29
+ }
30
+ }
31
+
32
+ private fun detectFileType(uri: String): Map<String, Any?> {
33
+ val parsed = Uri.parse(uri)
34
+ val fileName = parsed.lastPathSegment
35
+ val ext = fileName?.substringAfterLast('.', "")?.lowercase()?.takeIf { it.isNotEmpty() }
36
+ val type = when (ext) {
37
+ "kml" -> "kml"
38
+ "kmz" -> "kmz"
39
+ "zip" -> "zip"
40
+ "geojson" -> "geojson"
41
+ "json" -> "json"
42
+ else -> "unknown"
43
+ }
44
+ return mapOf("uri" to uri, "fileName" to fileName, "extension" to ext, "type" to type, "uti" to null)
45
+ }
46
+
47
+ private fun openStream(uri: String): InputStream {
48
+ val parsed = Uri.parse(uri)
49
+ if (parsed.scheme == "file") {
50
+ return File(parsed.path ?: throw GeoParserError.InvalidURI(uri)).inputStream()
51
+ }
52
+ val context = appContext.reactContext ?: throw GeoParserError.InvalidURI(uri)
53
+ return context.contentResolver.openInputStream(parsed) ?: throw GeoParserError.InvalidURI(uri)
54
+ }
55
+
56
+ private fun parseGeoFile(uri: String, onFeatures: (List<Map<String, Any?>>, Boolean) -> Unit): Map<String, Any?> {
57
+ val parsed = Uri.parse(uri)
58
+ val ext = parsed.lastPathSegment?.substringAfterLast('.', "")?.lowercase() ?: ""
59
+
60
+ if (ext == "kmz" || ext == "zip") return parseArchive(uri, ext, onFeatures)
61
+
62
+ return openStream(uri).use { stream ->
63
+ when (ext) {
64
+ "kml" -> KMLParser.parse(stream, "kml", onFeatures)
65
+ "geojson", "json" -> GeoJSONParser.parse(stream, ext, onFeatures)
66
+ else -> sniffAndParse(stream, "unknown", onFeatures)
67
+ }
68
+ }
69
+ }
70
+
71
+ private fun parseArchive(uri: String, sourceType: String, onFeatures: (List<Map<String, Any?>>, Boolean) -> Unit): Map<String, Any?> {
72
+ val context = appContext.reactContext ?: throw GeoParserError.InvalidURI(uri)
73
+ val tmpDir = File(context.cacheDir, UUID.randomUUID().toString()).also { it.mkdirs() }
74
+ try {
75
+ var bestKmlFile: File? = null
76
+ var firstGeoJsonFile: File? = null
77
+
78
+ openStream(uri).use { raw ->
79
+ ZipInputStream(raw).use { zip ->
80
+ var entry = zip.nextEntry
81
+ while (entry != null) {
82
+ val entryExt = entry.name.substringAfterLast('.', "").lowercase()
83
+ if (!entry.isDirectory) {
84
+ when (entryExt) {
85
+ "kml" -> {
86
+ val outFile = File(tmpDir, entry.name.substringAfterLast('/'))
87
+ outFile.parentFile?.mkdirs()
88
+ outFile.outputStream().use { zip.copyTo(it) }
89
+ if (bestKmlFile == null || entry.name.lowercase().endsWith("doc.kml")) {
90
+ bestKmlFile = outFile
91
+ }
92
+ }
93
+ "geojson", "json" -> {
94
+ if (firstGeoJsonFile == null) {
95
+ val outFile = File(tmpDir, entry.name.substringAfterLast('/'))
96
+ outFile.outputStream().use { zip.copyTo(it) }
97
+ firstGeoJsonFile = outFile
98
+ }
99
+ }
100
+ }
101
+ }
102
+ zip.closeEntry()
103
+ entry = zip.nextEntry
104
+ }
105
+ }
106
+ }
107
+
108
+ bestKmlFile?.let { file ->
109
+ return file.inputStream().use { KMLParser.parse(it, sourceType, onFeatures) }
110
+ }
111
+ firstGeoJsonFile?.let { file ->
112
+ return file.inputStream().use { GeoJSONParser.parse(it, sourceType, onFeatures) }
113
+ }
114
+
115
+ throw GeoParserError.NoGeoDataFound
116
+ } finally {
117
+ tmpDir.deleteRecursively()
118
+ }
119
+ }
120
+
121
+ private fun sniffAndParse(stream: InputStream, sourceType: String, onFeatures: (List<Map<String, Any?>>, Boolean) -> Unit): Map<String, Any?> {
122
+ val buffered = stream.buffered()
123
+ buffered.mark(512)
124
+ val peek = ByteArray(256)
125
+ val n = buffered.read(peek)
126
+ buffered.reset()
127
+ val prefix = if (n > 0) String(peek, 0, n, Charsets.UTF_8) else ""
128
+ return when {
129
+ prefix.trimStart().startsWith("{") -> GeoJSONParser.parse(buffered, sourceType, onFeatures)
130
+ "<kml" in prefix || "<Placemark" in prefix || "<Document" in prefix -> KMLParser.parse(buffered, sourceType, onFeatures)
131
+ else -> throw GeoParserError.UnsupportedFormat
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,9 @@
1
+ package expo.modules.geoparser
2
+
3
+ sealed class GeoParserError(message: String) : Exception(message) {
4
+ class InvalidURI(uri: String) : GeoParserError("Invalid URI: $uri")
5
+ object ExtractionFailed : GeoParserError("Failed to extract archive")
6
+ object NoGeoDataFound : GeoParserError("No supported geo data found in archive")
7
+ object UnsupportedFormat : GeoParserError("Unsupported or unrecognised file format")
8
+ class ParseError(msg: String) : GeoParserError("Parse error: $msg")
9
+ }
@@ -0,0 +1,100 @@
1
+ package expo.modules.geoparser.parsers
2
+
3
+ import expo.modules.geoparser.GeoParserError
4
+ import org.json.JSONArray
5
+ import org.json.JSONObject
6
+ import java.io.InputStream
7
+
8
+ object GeoJSONParser {
9
+
10
+ fun parse(input: InputStream, sourceType: String, onFeatures: (List<Map<String, Any?>>, isLast: Boolean) -> Unit): Map<String, Any?> {
11
+ val json = try {
12
+ JSONObject(input.reader(Charsets.UTF_8).readText())
13
+ } catch (e: Exception) {
14
+ throw GeoParserError.ParseError("Invalid JSON")
15
+ }
16
+
17
+ val features = extractFeatures(json)
18
+ val batchSize = 200
19
+ if (features.isEmpty()) {
20
+ onFeatures(emptyList(), true)
21
+ } else {
22
+ var idx = 0
23
+ while (idx < features.size) {
24
+ val end = minOf(idx + batchSize, features.size)
25
+ onFeatures(features.subList(idx, end), end >= features.size)
26
+ idx = end
27
+ }
28
+ }
29
+
30
+ val result = mutableMapOf<String, Any?>(
31
+ "type" to "FeatureCollection",
32
+ "sourceType" to sourceType
33
+ )
34
+ json.optString("name").takeIf { it.isNotEmpty() }?.let { result["name"] = it }
35
+ json.optJSONObject("properties")?.let { props ->
36
+ props.optString("name").takeIf { it.isNotEmpty() }?.let { result["name"] = it }
37
+ props.optString("description").takeIf { it.isNotEmpty() }?.let { result["description"] = it }
38
+ }
39
+
40
+ return result
41
+ }
42
+
43
+ private fun extractFeatures(json: JSONObject): List<Map<String, Any?>> {
44
+ return when (val type = json.optString("type")) {
45
+ "FeatureCollection" -> {
46
+ val arr = json.optJSONArray("features") ?: return emptyList()
47
+ (0 until arr.length()).mapNotNull { parseFeature(arr.getJSONObject(it)) }
48
+ }
49
+ "Feature" -> listOfNotNull(parseFeature(json))
50
+ else -> {
51
+ if (isGeometryType(type)) listOfNotNull(bareGeometryToFeature(json))
52
+ else emptyList()
53
+ }
54
+ }
55
+ }
56
+
57
+ private fun parseFeature(json: JSONObject): Map<String, Any?>? {
58
+ if (json.optString("type") != "Feature") return null
59
+ val geometry = json.optJSONObject("geometry") ?: return null
60
+ val properties = json.optJSONObject("properties") ?: JSONObject()
61
+ val feature = mutableMapOf<String, Any?>(
62
+ "type" to "Feature",
63
+ "geometry" to toMap(geometry),
64
+ "properties" to toMap(properties)
65
+ )
66
+ json.opt("id")?.takeIf { it != JSONObject.NULL }?.let { feature["id"] = it.toString() }
67
+ return feature
68
+ }
69
+
70
+ private fun bareGeometryToFeature(json: JSONObject): Map<String, Any?>? {
71
+ if (json.optString("type").isEmpty()) return null
72
+ return mapOf(
73
+ "type" to "Feature",
74
+ "geometry" to toMap(json),
75
+ "properties" to emptyMap<String, Any?>()
76
+ )
77
+ }
78
+
79
+ private fun isGeometryType(type: String): Boolean = type in setOf(
80
+ "Point", "LineString", "Polygon",
81
+ "MultiPoint", "MultiLineString", "MultiPolygon",
82
+ "GeometryCollection"
83
+ )
84
+
85
+ fun toMap(obj: JSONObject): Map<String, Any?> {
86
+ val map = mutableMapOf<String, Any?>()
87
+ for (key in obj.keys()) map[key] = toKotlin(obj.get(key))
88
+ return map
89
+ }
90
+
91
+ private fun toList(arr: JSONArray): List<Any?> =
92
+ (0 until arr.length()).map { toKotlin(arr.get(it)) }
93
+
94
+ private fun toKotlin(value: Any?): Any? = when (value) {
95
+ is JSONObject -> toMap(value)
96
+ is JSONArray -> toList(value)
97
+ JSONObject.NULL -> null
98
+ else -> value
99
+ }
100
+ }
@@ -0,0 +1,283 @@
1
+ package expo.modules.geoparser.parsers
2
+
3
+ import expo.modules.geoparser.GeoParserError
4
+ import org.xml.sax.Attributes
5
+ import org.xml.sax.helpers.DefaultHandler
6
+ import java.io.InputStream
7
+ import javax.xml.parsers.SAXParserFactory
8
+
9
+ class KMLParser(
10
+ private val batchSize: Int = 200,
11
+ private val onFeatures: (List<Map<String, Any?>>, isLast: Boolean) -> Unit
12
+ ) : DefaultHandler() {
13
+
14
+ private val featureBuffer = mutableListOf<Map<String, Any?>>()
15
+ var documentName = ""
16
+ var documentDescription = ""
17
+ private val styles = mutableMapOf<String, StyleInfo>()
18
+ private val styleMaps = mutableMapOf<String, String>()
19
+
20
+ private val elementStack = mutableListOf<String>()
21
+ private val textBuffer = StringBuilder()
22
+
23
+ private var documentDepth = 0
24
+ private var documentMetaCaptured = false
25
+
26
+ private var inPlacemark = false
27
+ private var currentFeatureId: String? = null
28
+ private var currentFeatureName = ""
29
+ private var currentFeatureDescription = ""
30
+ private var currentFeatureStyleUrl = ""
31
+ private var currentGeometryType: String? = null
32
+
33
+ private var pointCoord = listOf<Double>()
34
+ private var lineCoords = listOf<List<Double>>()
35
+ private var currentRing = listOf<List<Double>>()
36
+ private var outerRing = listOf<List<Double>>()
37
+ private var innerRings = mutableListOf<List<List<Double>>>()
38
+ private var inOuterBoundary = false
39
+ private var inInnerBoundary = false
40
+
41
+ private var inMultiGeometry = false
42
+ private var multiGeometries = mutableListOf<Map<String, Any?>>()
43
+
44
+ private var currentStyleId: String? = null
45
+ private var buildingStyle = StyleInfo()
46
+ private var inLineStyle = false
47
+ private var inPolyStyle = false
48
+ private var inIconStyle = false
49
+ private var inIconHref = false
50
+
51
+ private var inStyleMap = false
52
+ private var currentStyleMapId: String? = null
53
+ private var inPair = false
54
+ private var currentPairKey = ""
55
+ private var currentPairStyleUrl = ""
56
+
57
+ companion object {
58
+ fun parse(input: InputStream, sourceType: String, onFeatures: (List<Map<String, Any?>>, Boolean) -> Unit): Map<String, Any?> {
59
+ val handler = KMLParser(onFeatures = onFeatures)
60
+ val factory = SAXParserFactory.newInstance().apply { isNamespaceAware = true }
61
+ try {
62
+ factory.newSAXParser().parse(input, handler)
63
+ } catch (e: Exception) {
64
+ throw GeoParserError.ParseError(e.message ?: "XML parse error")
65
+ }
66
+ val result = mutableMapOf<String, Any?>(
67
+ "type" to "FeatureCollection",
68
+ "sourceType" to sourceType
69
+ )
70
+ if (handler.documentName.isNotEmpty()) result["name"] = handler.documentName
71
+ if (handler.documentDescription.isNotEmpty()) result["description"] = handler.documentDescription
72
+ return result
73
+ }
74
+ }
75
+
76
+ override fun startElement(uri: String?, localName: String?, qName: String?, attrs: Attributes?) {
77
+ val name = stripped(localName ?: qName ?: "")
78
+ elementStack.add(name)
79
+ textBuffer.clear()
80
+
81
+ when (name) {
82
+ "Document" -> documentDepth++
83
+ "Style" -> { currentStyleId = attrs?.getValue("id"); buildingStyle = StyleInfo() }
84
+ "StyleMap" -> { inStyleMap = true; currentStyleMapId = attrs?.getValue("id") }
85
+ "Pair" -> if (inStyleMap) { inPair = true; currentPairKey = ""; currentPairStyleUrl = "" }
86
+ "LineStyle" -> inLineStyle = true
87
+ "PolyStyle" -> inPolyStyle = true
88
+ "IconStyle" -> inIconStyle = true
89
+ "Icon" -> if (inIconStyle) inIconHref = true
90
+ "Placemark" -> {
91
+ inPlacemark = true
92
+ currentFeatureId = attrs?.getValue("id")
93
+ currentFeatureName = ""
94
+ currentFeatureDescription = ""
95
+ currentFeatureStyleUrl = ""
96
+ currentGeometryType = null
97
+ multiGeometries = mutableListOf()
98
+ }
99
+ "Point" -> { currentGeometryType = "Point"; pointCoord = listOf() }
100
+ "LineString" -> { currentGeometryType = "LineString"; lineCoords = listOf() }
101
+ "LinearRing" -> currentRing = listOf()
102
+ "Polygon" -> {
103
+ currentGeometryType = "Polygon"
104
+ outerRing = listOf(); innerRings = mutableListOf()
105
+ inOuterBoundary = false; inInnerBoundary = false
106
+ }
107
+ "MultiGeometry" -> { inMultiGeometry = true; multiGeometries = mutableListOf() }
108
+ "outerBoundaryIs" -> { inOuterBoundary = true; inInnerBoundary = false }
109
+ "innerBoundaryIs" -> { inInnerBoundary = true; inOuterBoundary = false }
110
+ }
111
+ }
112
+
113
+ override fun characters(ch: CharArray?, start: Int, length: Int) {
114
+ if (ch != null) textBuffer.append(ch, start, length)
115
+ }
116
+
117
+ override fun endElement(uri: String?, localName: String?, qName: String?) {
118
+ val name = stripped(localName ?: qName ?: "")
119
+ val text = textBuffer.toString().trim()
120
+ textBuffer.clear()
121
+ if (elementStack.isNotEmpty()) elementStack.removeAt(elementStack.lastIndex)
122
+
123
+ when (name) {
124
+ "Document" -> documentDepth--
125
+ "name" -> when {
126
+ inPlacemark -> currentFeatureName = text
127
+ documentDepth > 0 && !documentMetaCaptured -> { documentName = text; documentMetaCaptured = true }
128
+ }
129
+ "description" -> when {
130
+ inPlacemark -> currentFeatureDescription = text
131
+ documentDepth > 0 && documentDescription.isEmpty() -> documentDescription = text
132
+ }
133
+ "Style" -> { currentStyleId?.let { styles[it] = buildingStyle }; currentStyleId = null }
134
+ "StyleMap" -> { inStyleMap = false; currentStyleMapId = null }
135
+ "Pair" -> {
136
+ if (inStyleMap && currentPairKey == "normal") currentStyleMapId?.let { styleMaps[it] = currentPairStyleUrl }
137
+ inPair = false
138
+ }
139
+ "key" -> if (inStyleMap && inPair) currentPairKey = text
140
+ "styleUrl" -> when {
141
+ inPlacemark -> currentFeatureStyleUrl = text
142
+ inStyleMap && inPair -> currentPairStyleUrl = text
143
+ }
144
+ "LineStyle" -> inLineStyle = false
145
+ "PolyStyle" -> inPolyStyle = false
146
+ "IconStyle" -> inIconStyle = false
147
+ "Icon" -> inIconHref = false
148
+ "color" -> {
149
+ val hex = kmlColorToCSS(text)
150
+ when {
151
+ inLineStyle -> buildingStyle.strokeColor = hex
152
+ inPolyStyle -> buildingStyle.fillColor = hex
153
+ }
154
+ }
155
+ "width" -> if (inLineStyle) text.toDoubleOrNull()?.let { buildingStyle.strokeWidth = it }
156
+ "fill" -> if (inPolyStyle) buildingStyle.fillEnabled = text != "0"
157
+ "href" -> if (inIconHref) buildingStyle.iconUrl = text
158
+ "scale" -> if (inIconStyle) text.toDoubleOrNull()?.let { buildingStyle.iconScale = it }
159
+ "coordinates" -> {
160
+ val coords = parseCoordinates(text)
161
+ val parent = elementStack.lastOrNull() ?: ""
162
+ when (parent) {
163
+ "Point" -> pointCoord = coords.firstOrNull() ?: listOf()
164
+ "LineString" -> lineCoords = coords
165
+ "LinearRing" -> currentRing = coords
166
+ }
167
+ }
168
+ "LinearRing" -> when {
169
+ inOuterBoundary -> outerRing = currentRing
170
+ inInnerBoundary -> innerRings.add(currentRing)
171
+ }
172
+ "outerBoundaryIs" -> inOuterBoundary = false
173
+ "innerBoundaryIs" -> inInnerBoundary = false
174
+ "Point", "LineString", "Polygon" -> {
175
+ if (inMultiGeometry) {
176
+ buildGeometry(name)?.let {
177
+ multiGeometries.add(it)
178
+ pointCoord = listOf(); lineCoords = listOf()
179
+ outerRing = listOf(); innerRings = mutableListOf()
180
+ }
181
+ }
182
+ }
183
+ "MultiGeometry" -> { inMultiGeometry = false; currentGeometryType = "MultiGeometry" }
184
+ "Placemark" -> { finalizeFeature(); inPlacemark = false }
185
+ }
186
+ }
187
+
188
+ override fun endDocument() {
189
+ onFeatures(featureBuffer.toList(), true)
190
+ featureBuffer.clear()
191
+ }
192
+
193
+ private fun stripped(name: String): String {
194
+ val idx = name.lastIndexOf(':')
195
+ return if (idx >= 0) name.substring(idx + 1) else name
196
+ }
197
+
198
+ private fun kmlColorToCSS(kml: String): String {
199
+ val s = kml.trim().lowercase()
200
+ if (s.length != 8) return "#000000"
201
+ return "#${s.substring(6, 8)}${s.substring(4, 6)}${s.substring(2, 4)}"
202
+ }
203
+
204
+ private fun parseCoordinates(text: String): List<List<Double>> =
205
+ text.trim().split(Regex("\\s+"))
206
+ .filter { it.isNotEmpty() }
207
+ .mapNotNull { tuple ->
208
+ val parts = tuple.split(",").mapNotNull { it.toDoubleOrNull() }
209
+ if (parts.size >= 2) parts else null
210
+ }
211
+
212
+ private fun buildGeometry(type: String): Map<String, Any?>? = when (type) {
213
+ "Point" -> if (pointCoord.isNotEmpty()) mapOf("type" to "Point", "coordinates" to pointCoord) else null
214
+ "LineString" -> if (lineCoords.isNotEmpty()) mapOf("type" to "LineString", "coordinates" to lineCoords) else null
215
+ "Polygon" -> if (outerRing.isNotEmpty()) mapOf("type" to "Polygon", "coordinates" to (listOf(outerRing) + innerRings)) else null
216
+ else -> null
217
+ }
218
+
219
+ private fun resolveStyle(url: String): StyleInfo? {
220
+ val id = if (url.startsWith("#")) url.drop(1) else url
221
+ styles[id]?.let { return it }
222
+ styleMaps[id]?.let { normalUrl ->
223
+ val nid = if (normalUrl.startsWith("#")) normalUrl.drop(1) else normalUrl
224
+ return styles[nid]
225
+ }
226
+ return null
227
+ }
228
+
229
+ private fun finalizeFeature() {
230
+ val geometry: Map<String, Any?> = if (currentGeometryType == "MultiGeometry") {
231
+ if (multiGeometries.isEmpty()) return
232
+ mapOf("type" to "GeometryCollection", "geometries" to multiGeometries.toList())
233
+ } else {
234
+ val gType = currentGeometryType ?: return
235
+ buildGeometry(gType) ?: return
236
+ }
237
+
238
+ val properties = mutableMapOf<String, Any?>()
239
+ if (currentFeatureName.isNotEmpty()) properties["name"] = currentFeatureName
240
+ if (currentFeatureDescription.isNotEmpty()) properties["description"] = currentFeatureDescription
241
+ if (currentFeatureStyleUrl.isNotEmpty()) {
242
+ properties["styleId"] = currentFeatureStyleUrl
243
+ resolveStyle(currentFeatureStyleUrl)?.takeIf { !it.isEmpty }?.let {
244
+ properties["style"] = it.asDictionary()
245
+ }
246
+ }
247
+
248
+ val feature = mutableMapOf<String, Any?>(
249
+ "type" to "Feature",
250
+ "geometry" to geometry,
251
+ "properties" to properties
252
+ )
253
+ currentFeatureId?.takeIf { it.isNotEmpty() }?.let { feature["id"] = it }
254
+
255
+ featureBuffer.add(feature)
256
+ if (featureBuffer.size >= batchSize) {
257
+ onFeatures(featureBuffer.toList(), false)
258
+ featureBuffer.clear()
259
+ }
260
+ }
261
+ }
262
+
263
+ private data class StyleInfo(
264
+ var strokeColor: String? = null,
265
+ var strokeWidth: Double? = null,
266
+ var fillColor: String? = null,
267
+ var fillEnabled: Boolean = true,
268
+ var iconUrl: String? = null,
269
+ var iconScale: Double? = null
270
+ ) {
271
+ val isEmpty: Boolean get() = strokeColor == null && fillColor == null && iconUrl == null
272
+
273
+ fun asDictionary(): Map<String, Any?> {
274
+ val d = mutableMapOf<String, Any?>()
275
+ strokeColor?.let { d["strokeColor"] = it }
276
+ strokeWidth?.let { d["strokeWidth"] = it }
277
+ fillColor?.let { d["fillColor"] = it }
278
+ if (!fillEnabled) d["fillEnabled"] = false
279
+ iconUrl?.let { d["iconUrl"] = it }
280
+ iconScale?.let { d["iconScale"] = it }
281
+ return d
282
+ }
283
+ }
@@ -0,0 +1,2 @@
1
+ export type ExpoGeoParserEvents = {};
2
+ //# sourceMappingURL=ExpoGeoParser.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoGeoParser.types.d.ts","sourceRoot":"","sources":["../src/ExpoGeoParser.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ExpoGeoParser.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoGeoParser.types.js","sourceRoot":"","sources":["../src/ExpoGeoParser.types.ts"],"names":[],"mappings":"","sourcesContent":["export type ExpoGeoParserEvents = {};\n"]}
@@ -0,0 +1,41 @@
1
+ import { NativeModule } from "expo";
2
+ export type GeoFileType = "kml" | "kmz" | "zip" | "geojson" | "json" | "unknown";
3
+ export type DetectedFileType = {
4
+ uri: string;
5
+ fileName?: string | null;
6
+ extension?: string | null;
7
+ type: GeoFileType;
8
+ uti?: string | null;
9
+ };
10
+ export type GeoJSONGeometry = {
11
+ type: string;
12
+ coordinates?: unknown;
13
+ geometries?: GeoJSONGeometry[];
14
+ };
15
+ export type GeoJSONFeature = {
16
+ type: "Feature";
17
+ id?: string | number;
18
+ geometry: GeoJSONGeometry;
19
+ properties: Record<string, unknown>;
20
+ };
21
+ export type GeoJSONFeatureCollection = {
22
+ type: "FeatureCollection";
23
+ name?: string;
24
+ description?: string;
25
+ sourceType?: GeoFileType;
26
+ features: GeoJSONFeature[];
27
+ };
28
+ export type ParseFeaturesEvent = {
29
+ features: GeoJSONFeature[];
30
+ isLast: boolean;
31
+ };
32
+ declare class ExpoGeoParserModule extends NativeModule {
33
+ detectFileType(uri: string): DetectedFileType;
34
+ parseFile(uri: string): Promise<Omit<GeoJSONFeatureCollection, "features">>;
35
+ addListener(event: "onParseFeatures", listener: (event: ParseFeaturesEvent) => void): {
36
+ remove: () => void;
37
+ };
38
+ }
39
+ declare const _default: ExpoGeoParserModule;
40
+ export default _default;
41
+ //# sourceMappingURL=ExpoGeoParserModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoGeoParserModule.d.ts","sourceRoot":"","sources":["../src/ExpoGeoParserModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAEjF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,QAAQ,EAAE,eAAe,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,WAAW,CAAC;IACzB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,OAAO,OAAO,mBAAoB,SAAQ,YAAY;IACpD,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB;IAC7C,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,wBAAwB,EAAE,UAAU,CAAC,CAAC;IAC3E,WAAW,CACT,KAAK,EAAE,iBAAiB,EACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAC5C;QAAE,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE;CAC1B;;AAED,wBAAyE"}
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule } from "expo";
2
+ export default requireNativeModule("ExpoGeoParser");
3
+ //# sourceMappingURL=ExpoGeoParserModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoGeoParserModule.js","sourceRoot":"","sources":["../src/ExpoGeoParserModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AA+CzD,eAAe,mBAAmB,CAAsB,eAAe,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\n\nexport type GeoFileType = \"kml\" | \"kmz\" | \"zip\" | \"geojson\" | \"json\" | \"unknown\";\n\nexport type DetectedFileType = {\n uri: string;\n fileName?: string | null;\n extension?: string | null;\n type: GeoFileType;\n uti?: string | null;\n};\n\nexport type GeoJSONGeometry = {\n type: string;\n coordinates?: unknown;\n geometries?: GeoJSONGeometry[];\n};\n\nexport type GeoJSONFeature = {\n type: \"Feature\";\n id?: string | number;\n geometry: GeoJSONGeometry;\n properties: Record<string, unknown>;\n};\n\nexport type GeoJSONFeatureCollection = {\n type: \"FeatureCollection\";\n name?: string;\n description?: string;\n sourceType?: GeoFileType;\n features: GeoJSONFeature[];\n};\n\nexport type ParseFeaturesEvent = {\n features: GeoJSONFeature[];\n isLast: boolean;\n};\n\ndeclare class ExpoGeoParserModule extends NativeModule {\n detectFileType(uri: string): DetectedFileType;\n parseFile(uri: string): Promise<Omit<GeoJSONFeatureCollection, \"features\">>;\n addListener(\n event: \"onParseFeatures\",\n listener: (event: ParseFeaturesEvent) => void\n ): { remove: () => void };\n}\n\nexport default requireNativeModule<ExpoGeoParserModule>(\"ExpoGeoParser\");\n"]}