|
109 | 109 | return { title, properties } |
110 | 110 | } |
111 | 111 |
|
| 112 | +// Flatten nested objects using dot notation (mirrors KDK's dotify) |
| 113 | +function dotify (obj, prefix, result) { |
| 114 | + prefix = prefix || '' |
| 115 | + result = result || {} |
| 116 | + for (const [key, value] of Object.entries(obj)) { |
| 117 | + const fullKey = prefix ? `${prefix}.${key}` : key |
| 118 | + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { |
| 119 | + dotify(value, fullKey, result) |
| 120 | + } else { |
| 121 | + result[fullKey] = value |
| 122 | + } |
| 123 | + } |
| 124 | + return result |
| 125 | +} |
| 126 | + |
| 127 | +function getGeoJsonFeatures (geoJson) { |
| 128 | + if (geoJson.type === 'FeatureCollection') return geoJson.features || [] |
| 129 | + if (geoJson.type === 'Feature') return [geoJson] |
| 130 | + return [] |
| 131 | +} |
| 132 | + |
| 133 | +function isGeoJson (obj) { |
| 134 | + return obj.type === 'FeatureCollection' || obj.type === 'Feature' |
| 135 | +} |
| 136 | + |
| 137 | +// Infer properties from GeoJSON features, mirroring KDK's generatePropertiesSchema |
| 138 | +function parseGeoJson (geoJson) { |
| 139 | + const features = getGeoJsonFeatures(geoJson) |
| 140 | + // Collect first non-null value seen for each key across all features |
| 141 | + const propValues = {} |
| 142 | + for (const feature of features) { |
| 143 | + const props = feature.properties ? dotify(feature.properties) : {} |
| 144 | + for (const [key, value] of Object.entries(props)) { |
| 145 | + if (propValues[key] !== undefined && propValues[key] !== null) continue |
| 146 | + propValues[key] = value |
| 147 | + } |
| 148 | + } |
| 149 | + const properties = [] |
| 150 | + for (const [key, value] of Object.entries(propValues)) { |
| 151 | + let type = typeof value |
| 152 | + if (type === 'object' || type === 'undefined') type = 'string' |
| 153 | + properties.push({ name: key, type }) |
| 154 | + } |
| 155 | + return properties |
| 156 | +} |
| 157 | + |
112 | 158 | const App = { |
113 | 159 | template: ` |
114 | 160 | <q-layout view="hHh lpR fFf"> |
|
143 | 189 | > |
144 | 190 | <q-icon name="upload_file" size="48px" color="grey-6" /> |
145 | 191 | <div class="q-mt-sm text-grey-7"> |
146 | | - Drop a JSON schema file here or <strong>click to browse</strong> |
| 192 | + Drop a <strong>JSON schema</strong> or <strong>GeoJSON</strong> file here, or <strong>click to browse</strong> |
147 | 193 | </div> |
148 | | - <input ref="fileInput" type="file" accept=".json" style="display:none" @change="onFileChange" /> |
| 194 | + <div class="text-caption text-grey-5 q-mt-xs">Schema properties will be induced automatically from GeoJSON feature properties</div> |
| 195 | + <input ref="fileInput" type="file" accept=".json,.geojson" style="display:none" @change="onFileChange" /> |
149 | 196 | </div> |
150 | 197 | <div class="row q-mt-sm q-gutter-sm"> |
151 | 198 | <q-btn outline color="negative" icon="restart_alt" label="Start from scratch" @click="resetSchema" /> |
|
324 | 371 | <q-item-section avatar><q-icon name="upload_file" color="primary" /></q-item-section> |
325 | 372 | <q-item-section>Drop or select an existing Layer JSON schema file to edit it.</q-item-section> |
326 | 373 | </q-item> |
| 374 | + <q-item> |
| 375 | + <q-item-section avatar><q-icon name="map" color="primary" /></q-item-section> |
| 376 | + <q-item-section>Drop a GeoJSON file to automatically induce the schema from its feature properties.</q-item-section> |
| 377 | + </q-item> |
327 | 378 | <q-item> |
328 | 379 | <q-item-section avatar><q-icon name="add_circle" color="primary" /></q-item-section> |
329 | 380 | <q-item-section>Add properties by entering a name, choosing a type (string / number / boolean) and clicking +.</q-item-section> |
|
426 | 477 | $q.notify({ type: 'info', message: 'Schema cleared.' }) |
427 | 478 | } |
428 | 479 |
|
429 | | - function loadFromObject (obj) { |
| 480 | + function loadFromSchema (obj) { |
430 | 481 | try { |
431 | 482 | const parsed = parseSchema(obj) |
432 | 483 | schemaTitle.value = parsed.title |
|
438 | 489 | } |
439 | 490 | } |
440 | 491 |
|
| 492 | + function loadFromGeoJson (obj, filename) { |
| 493 | + try { |
| 494 | + const inferred = parseGeoJson(obj) |
| 495 | + properties.value = inferred |
| 496 | + // Suggest a title from filename if none set yet |
| 497 | + if (!schemaTitle.value && filename) { |
| 498 | + schemaTitle.value = filename.replace(/\.(geo)?json$/i, '') |
| 499 | + } |
| 500 | + cancelEdit() |
| 501 | + $q.notify({ type: 'positive', message: `Induced ${inferred.length} properties from GeoJSON.` }) |
| 502 | + } catch (e) { |
| 503 | + $q.notify({ type: 'negative', message: 'Failed to parse GeoJSON: ' + e.message }) |
| 504 | + } |
| 505 | + } |
| 506 | + |
441 | 507 | function readFile (file) { |
442 | 508 | if (!file) return |
443 | 509 | const reader = new FileReader() |
444 | 510 | reader.onload = (e) => { |
445 | 511 | try { |
446 | 512 | const obj = JSON.parse(e.target.result) |
447 | | - loadFromObject(obj) |
| 513 | + if (isGeoJson(obj)) { |
| 514 | + loadFromGeoJson(obj, file.name) |
| 515 | + } else { |
| 516 | + loadFromSchema(obj) |
| 517 | + } |
448 | 518 | } catch { |
449 | 519 | $q.notify({ type: 'negative', message: 'Invalid JSON file.' }) |
450 | 520 | } |
|
460 | 530 | function onDrop (event) { |
461 | 531 | dragging.value = false |
462 | 532 | const file = event.dataTransfer.files[0] |
463 | | - if (file && file.name.endsWith('.json')) { |
| 533 | + if (file && (file.name.endsWith('.json') || file.name.endsWith('.geojson'))) { |
464 | 534 | readFile(file) |
465 | 535 | } else { |
466 | | - $q.notify({ type: 'warning', message: 'Please drop a .json file.' }) |
| 536 | + $q.notify({ type: 'warning', message: 'Please drop a .json or .geojson file.' }) |
467 | 537 | } |
468 | 538 | } |
469 | 539 |
|
|
498 | 568 | typeOptions, dragging, showHelp, editingIdx, editingName, |
499 | 569 | schemaPreview, canExport, |
500 | 570 | addProperty, removeProperty, startEdit, saveEdit, cancelEdit, |
501 | | - resetSchema, onFileChange, onDrop, exportSchema, copySchema |
| 571 | + resetSchema, onFileChange, onDrop, exportSchema, copySchema, |
| 572 | + loadFromGeoJson |
502 | 573 | } |
503 | 574 | } |
504 | 575 | } |
|
0 commit comments