geoai 1.0.6 → 1.0.8
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/CHANGELOG.md +20 -0
- package/README.md +169 -170
- package/geoai.js +495 -1
- package/index.d.ts +175 -5
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.8] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Image geolocation task (`image-geolocation`) powered by GeoCLIP Q4 (`Xenova/geoclip-large-patch14`)
|
|
14
|
+
- Live demo and docs for image geolocation (`/tasks/image-geolocation`)
|
|
15
|
+
- Google Maps provider (`provider: "google"`) via Map Tiles API session + 2D satellite XYZ tiles
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- OAM item tile URLs: drop deprecated HOT TiTiler `@1x` path suffix (was returning HTTP 422)
|
|
19
|
+
|
|
20
|
+
## [1.0.7] - 2026-07-17
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- OpenAerialMap (OAM) map provider — HOT Imagery STAC + TiTiler (`provider: "oam"`)
|
|
24
|
+
- `examples/07-oam-quickstart` — Rome orthophoto draw-to-detect demo
|
|
25
|
+
- OAM option in `live-examples-nextjs` (flies to Rome; mosaic tiles)
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Docs Vercel prebuild: ship `remark` deps with docs and run `generate-llms-txt` from `docs/scripts/` so module resolution works when the Vercel root is `docs/`
|
|
29
|
+
|
|
10
30
|
## [1.0.6] - 2026-07-16
|
|
11
31
|
|
|
12
32
|
### Added
|
package/README.md
CHANGED
|
@@ -1,226 +1,224 @@
|
|
|
1
1
|
<h1 align="center">
|
|
2
|
-
GeoAI
|
|
2
|
+
GeoAI.js
|
|
3
3
|
</h1>
|
|
4
4
|
|
|
5
|
-
<p align="center"
|
|
5
|
+
<p align="center">
|
|
6
|
+
<strong>Geospatial AI that runs in the browser.</strong><br/>
|
|
7
|
+
Detect buildings, vehicles, ships, solar panels, and more — on satellite & aerial imagery — without a GPU backend.
|
|
8
|
+
</p>
|
|
6
9
|
|
|
7
|
-
<p align="center"
|
|
10
|
+
<p align="center">
|
|
8
11
|
<a href="https://www.npmjs.com/package/geoai"><img alt="npm version" src="https://img.shields.io/npm/v/geoai.svg"></a>
|
|
9
|
-
<a href="https://github.com/decision-labs/geoai.js/actions/workflows/main.yml"
|
|
10
|
-
<a href="https://www.npmjs.com/package/geoai"
|
|
11
|
-
<a href="https://docs.geobase.app/geoai"
|
|
12
|
+
<a href="https://github.com/decision-labs/geoai.js/actions/workflows/main.yml"><img alt="CI" src="https://github.com/decision-labs/geoai.js/actions/workflows/main.yml/badge.svg"></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/geoai"><img alt="npm downloads" src="https://img.shields.io/npm/dm/geoai.svg"></a>
|
|
14
|
+
<a href="https://docs.geobase.app/geoai"><img alt="docs" src="https://img.shields.io/badge/docs-online-blue"></a>
|
|
15
|
+
<a href="LICENSE.md"><img alt="license" src="https://img.shields.io/badge/license-MIT-green"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="https://docs.geobase.app/geoai-live"><strong>Live demos →</strong></a>
|
|
20
|
+
·
|
|
21
|
+
<a href="https://docs.geobase.app/geoai"><strong>Docs →</strong></a>
|
|
22
|
+
·
|
|
23
|
+
<a href="#agents--llms"><strong>Agent skill →</strong></a>
|
|
12
24
|
</p>
|
|
13
25
|
|
|
14
26
|
<div align="center">
|
|
15
|
-
<img src="_docs4devs/geoai.gif" alt="GeoAI
|
|
27
|
+
<img src="_docs4devs/geoai.gif" alt="GeoAI.js DINOv3 feature extraction demo" width="85%">
|
|
16
28
|
</div>
|
|
17
29
|
|
|
18
|
-
<p align="center"
|
|
19
|
-
Caption: <i>example of feature extraction using the transformerjs version of dino v3, see examples at <a href="https://docs.geobase.app/geoai-live/" style="font-style:normal;">docs.geobase.app/geoai-live</a></i>
|
|
20
|
-
</p>
|
|
30
|
+
<p align="center"><em>DINOv3 image feature extraction in the browser — try it at <a href="https://docs.geobase.app/geoai-live">docs.geobase.app/geoai-live</a></em></p>
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
---
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
## Why GeoAI.js?
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
| | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| **Client-side** | Models run in the browser (WebGPU / WASM) via Transformers.js + ONNX Runtime |
|
|
39
|
+
| **Draw → detect** | Pass a GeoJSON polygon; get detections back as GeoJSON |
|
|
40
|
+
| **Your imagery** | ESRI, Mapbox, Geobase COGs, TMS, WMS, OpenAerialMap, Google Map Tiles |
|
|
41
|
+
| **Pipeline-ready** | Chain tasks; run inference off the main thread with workers |
|
|
28
42
|
|
|
29
43
|
```bash
|
|
30
44
|
npm i geoai
|
|
45
|
+
# peers
|
|
46
|
+
npm i @huggingface/transformers onnxruntime-web
|
|
31
47
|
```
|
|
32
48
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- [jsDelivr CDN](https://www.jsdelivr.com/package/npm/geoai)
|
|
36
|
-
|
|
37
|
-
### CDN (Browser)
|
|
49
|
+
```javascript
|
|
50
|
+
import { geoai } from "geoai";
|
|
38
51
|
|
|
39
|
-
|
|
52
|
+
const pipeline = await geoai.pipeline([{ task: "building-detection" }], {
|
|
53
|
+
provider: "esri", // no API key
|
|
54
|
+
});
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
const result = await pipeline.inference({
|
|
57
|
+
inputs: { polygon: myGeoJsonPolygon },
|
|
58
|
+
mapSourceParams: { zoomLevel: 18 },
|
|
59
|
+
});
|
|
60
|
+
// result.detections → GeoJSON FeatureCollection
|
|
44
61
|
```
|
|
45
62
|
|
|
46
|
-
|
|
63
|
+
CDN:
|
|
64
|
+
|
|
47
65
|
```html
|
|
48
|
-
<script src="https://
|
|
66
|
+
<script src="https://unpkg.com/geoai@1.0.8/geoai.js"></script>
|
|
67
|
+
<!-- or -->
|
|
68
|
+
<script src="https://cdn.jsdelivr.net/npm/geoai@1.0.8/geoai.min.js"></script>
|
|
49
69
|
```
|
|
50
70
|
|
|
51
|
-
|
|
71
|
+
---
|
|
52
72
|
|
|
53
|
-
|
|
73
|
+
## Use cases
|
|
54
74
|
|
|
55
|
-
|
|
56
|
-
git init
|
|
57
|
-
touch README.md
|
|
58
|
-
git add .
|
|
59
|
-
git commit -m "Initial commit"
|
|
60
|
-
git subtree add --prefix=examples/01-quickstart https://github.com/decision-labs/geoai.js main --squash
|
|
61
|
-
```
|
|
75
|
+
GeoAI.js is built for product teams and researchers who need **interactive** geospatial AI — not batch jobs on a remote GPU cluster.
|
|
62
76
|
|
|
63
|
-
|
|
77
|
+
- **Urban inventory** — building footprints & boxes for planning, insurance, or change tracking
|
|
78
|
+
- **Energy & infrastructure** — solar panels, oil storage tanks, utility assets
|
|
79
|
+
- **Transport & logistics** — cars, trucks, ships in ports and yards
|
|
80
|
+
- **Environment** — land cover and wetland segmentation over AOIs
|
|
81
|
+
- **Exploration** — zero-shot detection, DINOv3 embeddings, and image geolocation (GeoCLIP)
|
|
64
82
|
|
|
65
|
-
|
|
83
|
+
Draw an AOI on a map, run a model, store or style the GeoJSON — all in one frontend session.
|
|
66
84
|
|
|
67
|
-
|
|
68
|
-
```javascript
|
|
69
|
-
import { geoai } from "geoai";
|
|
70
|
-
|
|
71
|
-
// Initialize the pipeline with ESRI provider (no API key required)
|
|
72
|
-
const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
|
|
73
|
-
provider: "esri",
|
|
74
|
-
});
|
|
85
|
+
### Image geolocation
|
|
75
86
|
|
|
76
|
-
|
|
87
|
+
```javascript
|
|
88
|
+
const pipeline = await geoai.pipeline(
|
|
89
|
+
[
|
|
90
|
+
{
|
|
91
|
+
task: "image-geolocation",
|
|
92
|
+
modelParams: { dtype: "q4", device: "webgpu" },
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
{ provider: "esri" }
|
|
96
|
+
);
|
|
77
97
|
const result = await pipeline.inference({
|
|
78
|
-
inputs: {
|
|
79
|
-
polygon: geoJsonFeature,
|
|
80
|
-
},
|
|
81
|
-
mapSourceParams: {
|
|
82
|
-
zoomLevel: 18,
|
|
83
|
-
},
|
|
98
|
+
inputs: { image: uploadedFile, topK: 5 },
|
|
84
99
|
});
|
|
100
|
+
// result.predictions → ranked [lat, lon] pairs
|
|
85
101
|
```
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
```javascript
|
|
89
|
-
// When using CDN, geoai is available globally
|
|
90
|
-
const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
|
|
91
|
-
provider: "esri",
|
|
92
|
-
});
|
|
103
|
+
See the [image geolocation docs](https://docs.geobase.app/geoai/supported-tasks/image-geolocation).
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Integrations
|
|
108
|
+
|
|
109
|
+
Persist detections, sync in real time, and serve results as vector tiles.
|
|
110
|
+
|
|
111
|
+
<div align="center">
|
|
112
|
+
<img
|
|
113
|
+
src="examples/04-geoai-supabase-geobase-integration/docs/screenshot.jpg"
|
|
114
|
+
alt="GeoAI.js + Supabase + Geobase — map, detections, and workflow panel"
|
|
115
|
+
width="85%"
|
|
116
|
+
>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<p align="center"><em>Full-stack demo: draw → detect → save to PostGIS (Supabase or Geobase) with live map layers</em></p>
|
|
103
120
|
|
|
104
|
-
|
|
121
|
+
| Stack | What you get |
|
|
122
|
+
|-------|----------------|
|
|
123
|
+
| **[Supabase](examples/04-geoai-supabase-geobase-integration)** | Auth, PostGIS storage, realtime subscriptions for detection history |
|
|
124
|
+
| **[Geobase](examples/04-geoai-supabase-geobase-integration)** | Same PostGIS path **plus** vector tileserver for thousands of styled detections |
|
|
125
|
+
| **[MapLibre / React](examples/live-examples-nextjs)** | Interactive task demos ([live](https://docs.geobase.app/geoai-live)); also [`01-quickstart`](examples/01-quickstart) |
|
|
126
|
+
| **[deck.gl](examples/deckgl-demo)** | GPU-friendly overlay workflows |
|
|
127
|
+
| **Agent skill** | Cursor / Claude / Codex integration via [`skills/geoai`](skills/geoai) |
|
|
128
|
+
|
|
129
|
+
Quick links:
|
|
130
|
+
|
|
131
|
+
- Example app + guide: [`examples/04-geoai-supabase-geobase-integration`](examples/04-geoai-supabase-geobase-integration)
|
|
132
|
+
- Integration write-up: [`INTEGRATION_GUIDE.md`](examples/04-geoai-supabase-geobase-integration/INTEGRATION_GUIDE.md)
|
|
133
|
+
- Interactive tasks: [docs.geobase.app/geoai-live](https://docs.geobase.app/geoai-live)
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Map providers
|
|
138
|
+
|
|
139
|
+
Point the pipeline at the imagery you already use:
|
|
140
|
+
|
|
141
|
+
| Provider | Auth | Notes |
|
|
142
|
+
|----------|------|--------|
|
|
143
|
+
| **ESRI** | None | World Imagery — great default for demos |
|
|
144
|
+
| **Mapbox** | Token | Satellite styles |
|
|
145
|
+
| **Geobase** | Project + API key | Your COG / tileserver |
|
|
146
|
+
| **TMS** | Optional | Any `{z}/{x}/{y}` raster URL |
|
|
147
|
+
| **WMS** | Usually none | OGC GetMap (e.g. NRW orthophotos) |
|
|
148
|
+
| **OpenAerialMap** | None | HOT Imagery STAC + TiTiler |
|
|
149
|
+
| **Google** | API key | Map Tiles API (session + XYZ); prefer a server proxy in browsers |
|
|
105
150
|
|
|
106
151
|
```javascript
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<div>
|
|
124
|
-
<button onClick={handleInference} disabled={isLoading}>
|
|
125
|
-
{isLoading ? "Processing..." : "Run Detection"}
|
|
126
|
-
</button>
|
|
127
|
-
{error && <div>Error: {error.message}</div>}
|
|
128
|
-
</div>
|
|
129
|
-
);
|
|
152
|
+
// Free public imagery
|
|
153
|
+
{ provider: "esri" }
|
|
154
|
+
|
|
155
|
+
// Community aerial (OAM)
|
|
156
|
+
{ provider: "oam" /* , itemId, mosaic: true */ }
|
|
157
|
+
|
|
158
|
+
// Your COG on Geobase
|
|
159
|
+
{
|
|
160
|
+
provider: "geobase",
|
|
161
|
+
projectRef: "...",
|
|
162
|
+
apikey: "...",
|
|
163
|
+
cogImagery: "https://path-to-your-cog.tif",
|
|
130
164
|
}
|
|
165
|
+
|
|
166
|
+
// Google Map Tiles (enable Map Tiles API; avoid HTTP-referrer–only keys)
|
|
167
|
+
{ provider: "google", apiKey: process.env.GOOGLE_MAPS_API_KEY }
|
|
131
168
|
```
|
|
132
169
|
|
|
133
|
-
|
|
170
|
+
Docs: [Map providers](https://docs.geobase.app/geoai/map-providers)
|
|
134
171
|
|
|
135
|
-
|
|
136
|
-
- **Map Provider Support**: Geobase, Mapbox, ESRI, TMS, and Google Maps
|
|
137
|
-
- **React Integration**: Hooks for easy React integration
|
|
138
|
-
- **TypeScript Support**: Full TypeScript definitions
|
|
139
|
-
- **Web Worker Support**: Run AI models in background threads
|
|
140
|
-
- **Optimized Performance**: Efficient model loading and inference
|
|
141
|
-
- **CDN Ready**: Available via npm and popular CDNs (unpkg, jsDelivr)
|
|
172
|
+
---
|
|
142
173
|
|
|
143
|
-
## Supported
|
|
174
|
+
## Supported tasks
|
|
144
175
|
|
|
145
|
-
-
|
|
146
|
-
- Building Detection
|
|
147
|
-
- Car Detection
|
|
148
|
-
- Ship Detection
|
|
149
|
-
- Solar Panel Detection
|
|
150
|
-
- Oil Storage Tank Detection
|
|
151
|
-
- Land Cover Classification
|
|
152
|
-
- Wetland Segmentation
|
|
153
|
-
- Building Footprint Segmentation
|
|
154
|
-
- Mask Generation
|
|
155
|
-
- Zero-shot Object Detection
|
|
156
|
-
- Zero-shot Segmentation
|
|
157
|
-
- Image Feature Extraction
|
|
176
|
+
Object detection · Building detection · Building footprint segmentation (ChangeStar) · Car · Ship · Solar panel · Oil storage tank · Oriented object detection · Land cover · Wetland · Mask generation · Zero-shot detection & segmentation · Image feature extraction (DINOv3)
|
|
158
177
|
|
|
159
|
-
|
|
178
|
+
→ [Full task reference](https://docs.geobase.app/geoai/supported-tasks)
|
|
160
179
|
|
|
161
|
-
|
|
180
|
+
---
|
|
162
181
|
|
|
163
|
-
|
|
182
|
+
## Workers & React
|
|
164
183
|
|
|
165
|
-
|
|
166
|
-
TMS is a tile-based map specification that uses a bottom-left origin coordinate system. Perfect for custom tile servers and OpenAerialMap.
|
|
184
|
+
Keep the UI smooth — run inference in a **Web Worker**. Patterns for vanilla JS and React live in the skill and docs (there is no separate `geoai/react` package export; use the worker + hook examples).
|
|
167
185
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
provider: "tms",
|
|
172
|
-
baseUrl: "https://tile.example.com/tiles/{z}/{x}/{y}.png",
|
|
173
|
-
apiKey: "your-api-key", // optional
|
|
174
|
-
tileSize: 256, // optional, defaults to 256
|
|
175
|
-
attribution: "Custom TMS Provider", // optional
|
|
176
|
-
});
|
|
186
|
+
- [Workers guide](https://docs.geobase.app/geoai/workers)
|
|
187
|
+
- [React quickstart](https://docs.geobase.app/geoai/frontend-frameworks/react-quickstart)
|
|
188
|
+
- Example: [`examples/02-quickstart-with-workers`](examples/02-quickstart-with-workers)
|
|
177
189
|
|
|
178
|
-
|
|
179
|
-
const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
|
|
180
|
-
provider: "tms",
|
|
181
|
-
baseUrl: "https://tile.example.com/tiles",
|
|
182
|
-
extension: "png", // optional, defaults to "png"
|
|
183
|
-
apiKey: "your-api-key", // optional
|
|
184
|
-
tileSize: 256, // optional, defaults to 256
|
|
185
|
-
attribution: "Custom TMS Provider", // optional
|
|
186
|
-
});
|
|
187
|
-
```
|
|
190
|
+
---
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
ESRI World Imagery - no API key required.
|
|
192
|
+
## Quickstart clone
|
|
191
193
|
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
```bash
|
|
195
|
+
git init
|
|
196
|
+
git subtree add --prefix=examples/01-quickstart \
|
|
197
|
+
https://github.com/decision-labs/geoai.js main --squash
|
|
198
|
+
cd examples/01-quickstart && npm install && npm run dev
|
|
196
199
|
```
|
|
197
200
|
|
|
198
|
-
|
|
199
|
-
Requires a Mapbox API key.
|
|
201
|
+
---
|
|
200
202
|
|
|
201
|
-
|
|
202
|
-
const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
|
|
203
|
-
provider: "mapbox",
|
|
204
|
-
apiKey: "your-mapbox-api-key",
|
|
205
|
-
style: "mapbox://styles/mapbox/satellite-v9",
|
|
206
|
-
});
|
|
207
|
-
```
|
|
203
|
+
## Agents & LLMs
|
|
208
204
|
|
|
209
|
-
|
|
210
|
-
Requires Geobase project credentials.
|
|
205
|
+
Teach coding agents how to integrate GeoAI.js:
|
|
211
206
|
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
apikey: "your-api-key",
|
|
217
|
-
cogImagery: "https://path-to-your-cog.tif",
|
|
218
|
-
});
|
|
207
|
+
```bash
|
|
208
|
+
npx skills add decision-labs/geoai.js --skill geoai -y
|
|
209
|
+
# or globally
|
|
210
|
+
npx skills add decision-labs/geoai.js --skill geoai -g -y
|
|
219
211
|
```
|
|
220
212
|
|
|
213
|
+
- Skill source: [`skills/geoai`](skills/geoai)
|
|
214
|
+
- LLM docs index: [llms.txt](https://docs.geobase.app/geoai/llms.txt)
|
|
215
|
+
- Full corpus: [llms-full.txt](https://docs.geobase.app/geoai/llms-full.txt) (`pnpm docs:llms`)
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
221
219
|
## Talk
|
|
222
220
|
|
|
223
|
-
**[Bringing Earth Observation AI to the Browser with WebGPU](talks/lightning-talk-webgpu-geoai.md)** —
|
|
221
|
+
**[Bringing Earth Observation AI to the Browser with WebGPU](talks/lightning-talk-webgpu-geoai.md)** — Big Data from Space 2025 (Decision Labs).
|
|
224
222
|
|
|
225
223
|
<div align="center">
|
|
226
224
|
<a href="https://geobase-docs.s3.amazonaws.com/geobase-ai-assets/talks/lightning-talk-webgpu-geoai.pdf">
|
|
@@ -229,23 +227,24 @@ const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
|
|
|
229
227
|
</div>
|
|
230
228
|
|
|
231
229
|
- [Slides (PDF)](https://geobase-docs.s3.amazonaws.com/geobase-ai-assets/talks/lightning-talk-webgpu-geoai.pdf)
|
|
232
|
-
|
|
230
|
+
|
|
231
|
+
---
|
|
233
232
|
|
|
234
233
|
## Links
|
|
235
234
|
|
|
236
|
-
- **
|
|
237
|
-
- **Live
|
|
238
|
-
- **
|
|
239
|
-
- **
|
|
240
|
-
- **
|
|
241
|
-
- **Issues
|
|
235
|
+
- **Docs** — [docs.geobase.app/geoai](https://docs.geobase.app/geoai)
|
|
236
|
+
- **Live examples** — [docs.geobase.app/geoai-live](https://docs.geobase.app/geoai-live)
|
|
237
|
+
- **npm** — [geoai](https://www.npmjs.com/package/geoai)
|
|
238
|
+
- **Roadmap** — [_docs4devs/ROADMAP.md](_docs4devs/ROADMAP.md)
|
|
239
|
+
- **Discussions** — [GitHub Discussions](https://github.com/decision-labs/geoai.js/discussions)
|
|
240
|
+
- **Issues** — [GitHub Issues](https://github.com/decision-labs/geoai.js/issues)
|
|
242
241
|
|
|
243
242
|
## Contributing
|
|
244
243
|
|
|
245
|
-
|
|
244
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
246
245
|
|
|
247
246
|
## License
|
|
248
247
|
|
|
249
|
-
MIT
|
|
248
|
+
MIT — [LICENSE.md](LICENSE.md)
|
|
250
249
|
|
|
251
250
|
[//]: <> (Toggle CI off)
|
package/geoai.js
CHANGED
|
@@ -390,7 +390,7 @@ const postProcessYoloOutput = (model_outputs, pixel_values, rawImage, id2label)
|
|
|
390
390
|
return model_output;
|
|
391
391
|
};
|
|
392
392
|
const parametersChanged = (instance, model_id, providerParams, modelParams) => {
|
|
393
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
393
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u;
|
|
394
394
|
if (instance.model_id !== model_id) {
|
|
395
395
|
return true;
|
|
396
396
|
}
|
|
@@ -425,6 +425,16 @@ const parametersChanged = (instance, model_id, providerParams, modelParams) => {
|
|
|
425
425
|
return true;
|
|
426
426
|
}
|
|
427
427
|
break;
|
|
428
|
+
case "oam":
|
|
429
|
+
if (((_m = instance.providerParams) == null ? void 0 : _m.stacUrl) !== (providerParams == null ? void 0 : providerParams.stacUrl) || ((_n = instance.providerParams) == null ? void 0 : _n.rasterUrl) !== (providerParams == null ? void 0 : providerParams.rasterUrl) || ((_o = instance.providerParams) == null ? void 0 : _o.collection) !== (providerParams == null ? void 0 : providerParams.collection) || ((_p = instance.providerParams) == null ? void 0 : _p.asset) !== (providerParams == null ? void 0 : providerParams.asset) || ((_q = instance.providerParams) == null ? void 0 : _q.itemId) !== (providerParams == null ? void 0 : providerParams.itemId) || ((_r = instance.providerParams) == null ? void 0 : _r.mosaic) !== (providerParams == null ? void 0 : providerParams.mosaic)) {
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
break;
|
|
433
|
+
case "google":
|
|
434
|
+
if (((_s = instance.providerParams) == null ? void 0 : _s.apiKey) !== (providerParams == null ? void 0 : providerParams.apiKey) || ((_t = instance.providerParams) == null ? void 0 : _t.mapType) !== (providerParams == null ? void 0 : providerParams.mapType) || ((_u = instance.providerParams) == null ? void 0 : _u.sessionToken) !== (providerParams == null ? void 0 : providerParams.sessionToken)) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
428
438
|
}
|
|
429
439
|
if (modelParams) {
|
|
430
440
|
const instanceModelParams = instance.modelParams || {};
|
|
@@ -1380,6 +1390,323 @@ class Wms extends MapSource {
|
|
|
1380
1390
|
);
|
|
1381
1391
|
}
|
|
1382
1392
|
}
|
|
1393
|
+
const DEFAULT_OAM_STAC_URL = "https://api.imagery.hotosm.org/stac";
|
|
1394
|
+
const DEFAULT_OAM_RASTER_URL = "https://api.imagery.hotosm.org/raster";
|
|
1395
|
+
const DEFAULT_OAM_COLLECTION = "openaerialmap";
|
|
1396
|
+
const DEFAULT_OAM_ASSET = "visual";
|
|
1397
|
+
function buildOamMosaicTileUrlTemplate(rasterUrl = DEFAULT_OAM_RASTER_URL, collection = DEFAULT_OAM_COLLECTION, asset = DEFAULT_OAM_ASSET) {
|
|
1398
|
+
const base = rasterUrl.replace(/\/$/, "");
|
|
1399
|
+
return `${base}/collections/${encodeURIComponent(collection)}/tiles/WebMercatorQuad/{z}/{x}/{y}?assets=${encodeURIComponent(asset)}`;
|
|
1400
|
+
}
|
|
1401
|
+
function buildOamItemTileUrlTemplate(itemId, rasterUrl = DEFAULT_OAM_RASTER_URL, collection = DEFAULT_OAM_COLLECTION, asset = DEFAULT_OAM_ASSET) {
|
|
1402
|
+
const base = rasterUrl.replace(/\/$/, "");
|
|
1403
|
+
return `${base}/collections/${encodeURIComponent(collection)}/items/${encodeURIComponent(itemId)}/tiles/WebMercatorQuad/{z}/{x}/{y}?assets=${encodeURIComponent(asset)}`;
|
|
1404
|
+
}
|
|
1405
|
+
function pickBestOamItem(features) {
|
|
1406
|
+
if (!features.length) return void 0;
|
|
1407
|
+
return features.slice().sort((a, b) => {
|
|
1408
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1409
|
+
const gsdA = typeof ((_a = a.properties) == null ? void 0 : _a.gsd) === "number" ? a.properties.gsd : Number.POSITIVE_INFINITY;
|
|
1410
|
+
const gsdB = typeof ((_b = b.properties) == null ? void 0 : _b.gsd) === "number" ? b.properties.gsd : Number.POSITIVE_INFINITY;
|
|
1411
|
+
if (gsdA !== gsdB) return gsdA - gsdB;
|
|
1412
|
+
const dateA = Date.parse(
|
|
1413
|
+
String(((_c = a.properties) == null ? void 0 : _c.created) || ((_d = a.properties) == null ? void 0 : _d.datetime) || 0)
|
|
1414
|
+
);
|
|
1415
|
+
const dateB = Date.parse(
|
|
1416
|
+
String(((_e = b.properties) == null ? void 0 : _e.created) || ((_f = b.properties) == null ? void 0 : _f.datetime) || 0)
|
|
1417
|
+
);
|
|
1418
|
+
return dateB - dateA;
|
|
1419
|
+
})[0];
|
|
1420
|
+
}
|
|
1421
|
+
async function searchOamItems(bbox2, options = {}) {
|
|
1422
|
+
const stacUrl = (options.stacUrl || DEFAULT_OAM_STAC_URL).replace(/\/$/, "");
|
|
1423
|
+
const collection = options.collection || DEFAULT_OAM_COLLECTION;
|
|
1424
|
+
const limit = options.limit ?? 10;
|
|
1425
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
1426
|
+
const response = await fetchImpl(`${stacUrl}/search`, {
|
|
1427
|
+
method: "POST",
|
|
1428
|
+
headers: {
|
|
1429
|
+
"Content-Type": "application/json",
|
|
1430
|
+
Accept: "application/geo+json",
|
|
1431
|
+
...options.headers || {}
|
|
1432
|
+
},
|
|
1433
|
+
body: JSON.stringify({
|
|
1434
|
+
collections: [collection],
|
|
1435
|
+
bbox: bbox2,
|
|
1436
|
+
limit
|
|
1437
|
+
})
|
|
1438
|
+
});
|
|
1439
|
+
if (!response.ok) {
|
|
1440
|
+
const body = await response.text().catch(() => "");
|
|
1441
|
+
throw new Error(
|
|
1442
|
+
`OAM STAC search failed (${response.status}): ${body || response.statusText}`
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
const data = await response.json();
|
|
1446
|
+
return data.features || [];
|
|
1447
|
+
}
|
|
1448
|
+
class Oam extends MapSource {
|
|
1449
|
+
constructor(config = {}) {
|
|
1450
|
+
super();
|
|
1451
|
+
__publicField(this, "stacUrl");
|
|
1452
|
+
__publicField(this, "rasterUrl");
|
|
1453
|
+
__publicField(this, "collection");
|
|
1454
|
+
__publicField(this, "asset");
|
|
1455
|
+
__publicField(this, "itemId");
|
|
1456
|
+
__publicField(this, "mosaic");
|
|
1457
|
+
__publicField(this, "attribution");
|
|
1458
|
+
__publicField(this, "tileSize");
|
|
1459
|
+
__publicField(this, "headers");
|
|
1460
|
+
/** Resolved XYZ template with `{z}/{x}/{y}` placeholders. */
|
|
1461
|
+
__publicField(this, "tileUrlTemplate");
|
|
1462
|
+
__publicField(this, "resolvedForBboxKey");
|
|
1463
|
+
this.stacUrl = (config.stacUrl || DEFAULT_OAM_STAC_URL).replace(/\/$/, "");
|
|
1464
|
+
this.rasterUrl = (config.rasterUrl || DEFAULT_OAM_RASTER_URL).replace(
|
|
1465
|
+
/\/$/,
|
|
1466
|
+
""
|
|
1467
|
+
);
|
|
1468
|
+
this.collection = config.collection || DEFAULT_OAM_COLLECTION;
|
|
1469
|
+
this.asset = config.asset || DEFAULT_OAM_ASSET;
|
|
1470
|
+
this.itemId = config.itemId;
|
|
1471
|
+
this.mosaic = config.mosaic === true;
|
|
1472
|
+
this.attribution = config.attribution || "OpenAerialMap / HOT Imagery";
|
|
1473
|
+
this.tileSize = config.tileSize || 256;
|
|
1474
|
+
this.headers = config.headers;
|
|
1475
|
+
if (this.itemId) {
|
|
1476
|
+
this.tileUrlTemplate = buildOamItemTileUrlTemplate(
|
|
1477
|
+
this.itemId,
|
|
1478
|
+
this.rasterUrl,
|
|
1479
|
+
this.collection,
|
|
1480
|
+
this.asset
|
|
1481
|
+
);
|
|
1482
|
+
} else if (this.mosaic) {
|
|
1483
|
+
this.tileUrlTemplate = buildOamMosaicTileUrlTemplate(
|
|
1484
|
+
this.rasterUrl,
|
|
1485
|
+
this.collection,
|
|
1486
|
+
this.asset
|
|
1487
|
+
);
|
|
1488
|
+
} else {
|
|
1489
|
+
this.tileUrlTemplate = buildOamMosaicTileUrlTemplate(
|
|
1490
|
+
this.rasterUrl,
|
|
1491
|
+
this.collection,
|
|
1492
|
+
this.asset
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Resolve tile URL template for an AOI via STAC (unless mosaic/itemId pin).
|
|
1498
|
+
*/
|
|
1499
|
+
async resolveTileTemplate(polygon2) {
|
|
1500
|
+
if (this.itemId) {
|
|
1501
|
+
this.tileUrlTemplate = buildOamItemTileUrlTemplate(
|
|
1502
|
+
this.itemId,
|
|
1503
|
+
this.rasterUrl,
|
|
1504
|
+
this.collection,
|
|
1505
|
+
this.asset
|
|
1506
|
+
);
|
|
1507
|
+
return this.tileUrlTemplate;
|
|
1508
|
+
}
|
|
1509
|
+
if (this.mosaic) {
|
|
1510
|
+
this.tileUrlTemplate = buildOamMosaicTileUrlTemplate(
|
|
1511
|
+
this.rasterUrl,
|
|
1512
|
+
this.collection,
|
|
1513
|
+
this.asset
|
|
1514
|
+
);
|
|
1515
|
+
return this.tileUrlTemplate;
|
|
1516
|
+
}
|
|
1517
|
+
const bbox$1 = bbox(polygon2);
|
|
1518
|
+
const bboxKey = bbox$1.map((n) => n.toFixed(6)).join(",");
|
|
1519
|
+
if (this.resolvedForBboxKey === bboxKey && this.tileUrlTemplate) {
|
|
1520
|
+
return this.tileUrlTemplate;
|
|
1521
|
+
}
|
|
1522
|
+
const features = await searchOamItems(bbox$1, {
|
|
1523
|
+
stacUrl: this.stacUrl,
|
|
1524
|
+
collection: this.collection,
|
|
1525
|
+
headers: this.headers
|
|
1526
|
+
});
|
|
1527
|
+
const best = pickBestOamItem(features);
|
|
1528
|
+
if (best == null ? void 0 : best.id) {
|
|
1529
|
+
this.itemId = best.id;
|
|
1530
|
+
this.tileUrlTemplate = buildOamItemTileUrlTemplate(
|
|
1531
|
+
best.id,
|
|
1532
|
+
this.rasterUrl,
|
|
1533
|
+
this.collection,
|
|
1534
|
+
this.asset
|
|
1535
|
+
);
|
|
1536
|
+
} else {
|
|
1537
|
+
this.tileUrlTemplate = buildOamMosaicTileUrlTemplate(
|
|
1538
|
+
this.rasterUrl,
|
|
1539
|
+
this.collection,
|
|
1540
|
+
this.asset
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
this.resolvedForBboxKey = bboxKey;
|
|
1544
|
+
return this.tileUrlTemplate;
|
|
1545
|
+
}
|
|
1546
|
+
async getImage(polygon2, bands, expression, zoomLevel, requiresSquare = false, stitch = true) {
|
|
1547
|
+
await this.resolveTileTemplate(polygon2);
|
|
1548
|
+
return super.getImage(
|
|
1549
|
+
polygon2,
|
|
1550
|
+
bands,
|
|
1551
|
+
expression,
|
|
1552
|
+
zoomLevel,
|
|
1553
|
+
requiresSquare,
|
|
1554
|
+
stitch
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
getTileUrlFromTileCoords(tileCoords, instance) {
|
|
1558
|
+
const [x, y, z] = tileCoords;
|
|
1559
|
+
return instance.tileUrlTemplate.replaceAll("{z}", z.toString()).replaceAll("{x}", x.toString()).replaceAll("{y}", y.toString());
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
const DEFAULT_GOOGLE_TILE_API_URL = "https://tile.googleapis.com";
|
|
1563
|
+
const DEFAULT_GOOGLE_MAP_TYPE = "satellite";
|
|
1564
|
+
function buildGoogleCreateSessionUrl(apiKey, tileApiUrl = DEFAULT_GOOGLE_TILE_API_URL, includeApiKey = true) {
|
|
1565
|
+
const base = tileApiUrl.replace(/\/$/, "");
|
|
1566
|
+
if (!includeApiKey) {
|
|
1567
|
+
return `${base}/v1/createSession`;
|
|
1568
|
+
}
|
|
1569
|
+
return `${base}/v1/createSession?key=${encodeURIComponent(apiKey)}`;
|
|
1570
|
+
}
|
|
1571
|
+
function buildGoogleTileUrl(z, x, y, sessionToken, apiKey, tileApiUrl = DEFAULT_GOOGLE_TILE_API_URL, includeApiKey = true) {
|
|
1572
|
+
const base = tileApiUrl.replace(/\/$/, "");
|
|
1573
|
+
let url = `${base}/v1/2dtiles/${z}/${x}/${y}?session=${encodeURIComponent(sessionToken)}`;
|
|
1574
|
+
if (includeApiKey) {
|
|
1575
|
+
url += `&key=${encodeURIComponent(apiKey)}`;
|
|
1576
|
+
}
|
|
1577
|
+
return url;
|
|
1578
|
+
}
|
|
1579
|
+
async function createGoogleSession(options) {
|
|
1580
|
+
const apiKey = options.apiKey;
|
|
1581
|
+
const includeApiKey = options.includeApiKey !== false;
|
|
1582
|
+
if (includeApiKey && !apiKey) {
|
|
1583
|
+
throw new Error("Google Maps provider requires an apiKey");
|
|
1584
|
+
}
|
|
1585
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
1586
|
+
const url = buildGoogleCreateSessionUrl(
|
|
1587
|
+
apiKey,
|
|
1588
|
+
options.tileApiUrl || DEFAULT_GOOGLE_TILE_API_URL,
|
|
1589
|
+
includeApiKey
|
|
1590
|
+
);
|
|
1591
|
+
const response = await fetchImpl(url, {
|
|
1592
|
+
method: "POST",
|
|
1593
|
+
headers: {
|
|
1594
|
+
"Content-Type": "application/json",
|
|
1595
|
+
...options.headers || {}
|
|
1596
|
+
},
|
|
1597
|
+
body: JSON.stringify({
|
|
1598
|
+
mapType: options.mapType || DEFAULT_GOOGLE_MAP_TYPE,
|
|
1599
|
+
language: options.language || "en-US",
|
|
1600
|
+
region: options.region || "US"
|
|
1601
|
+
})
|
|
1602
|
+
});
|
|
1603
|
+
if (!response.ok) {
|
|
1604
|
+
const body = await response.text().catch(() => "");
|
|
1605
|
+
throw new Error(
|
|
1606
|
+
`Google Map Tiles createSession failed (${response.status}): ${body || response.statusText}`
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
const data = await response.json();
|
|
1610
|
+
if (!data.session) {
|
|
1611
|
+
throw new Error("Google Map Tiles createSession returned no session token");
|
|
1612
|
+
}
|
|
1613
|
+
return data;
|
|
1614
|
+
}
|
|
1615
|
+
class GoogleMaps extends MapSource {
|
|
1616
|
+
constructor(config) {
|
|
1617
|
+
super();
|
|
1618
|
+
__publicField(this, "apiKey");
|
|
1619
|
+
__publicField(this, "mapType");
|
|
1620
|
+
__publicField(this, "language");
|
|
1621
|
+
__publicField(this, "region");
|
|
1622
|
+
__publicField(this, "tileApiUrl");
|
|
1623
|
+
__publicField(this, "includeApiKey");
|
|
1624
|
+
__publicField(this, "attribution");
|
|
1625
|
+
__publicField(this, "tileSize");
|
|
1626
|
+
__publicField(this, "headers");
|
|
1627
|
+
__publicField(this, "fetchImpl");
|
|
1628
|
+
__publicField(this, "sessionToken");
|
|
1629
|
+
/** Unix epoch seconds when the session expires. */
|
|
1630
|
+
__publicField(this, "sessionExpiry");
|
|
1631
|
+
this.includeApiKey = config.includeApiKey !== false;
|
|
1632
|
+
if (this.includeApiKey && !config.apiKey) {
|
|
1633
|
+
throw new Error("Google Maps provider requires an apiKey");
|
|
1634
|
+
}
|
|
1635
|
+
this.apiKey = config.apiKey || "";
|
|
1636
|
+
this.mapType = config.mapType || DEFAULT_GOOGLE_MAP_TYPE;
|
|
1637
|
+
this.language = config.language || "en-US";
|
|
1638
|
+
this.region = config.region || "US";
|
|
1639
|
+
this.tileApiUrl = (config.tileApiUrl || DEFAULT_GOOGLE_TILE_API_URL).replace(/\/$/, "");
|
|
1640
|
+
this.attribution = config.attribution || "© Google Maps / Map Tiles API";
|
|
1641
|
+
this.tileSize = config.tileSize || 256;
|
|
1642
|
+
this.headers = config.headers;
|
|
1643
|
+
this.fetchImpl = config.fetchImpl || fetch;
|
|
1644
|
+
this.sessionToken = config.sessionToken;
|
|
1645
|
+
}
|
|
1646
|
+
isSessionValid(skewSeconds = 60) {
|
|
1647
|
+
if (!this.sessionToken) {
|
|
1648
|
+
return false;
|
|
1649
|
+
}
|
|
1650
|
+
if (this.sessionExpiry == null) {
|
|
1651
|
+
return true;
|
|
1652
|
+
}
|
|
1653
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1654
|
+
return this.sessionExpiry - skewSeconds > now;
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Ensure a valid Map Tiles session exists (create or refresh).
|
|
1658
|
+
*/
|
|
1659
|
+
async ensureSession() {
|
|
1660
|
+
if (this.isSessionValid()) {
|
|
1661
|
+
return this.sessionToken;
|
|
1662
|
+
}
|
|
1663
|
+
const data = await createGoogleSession({
|
|
1664
|
+
apiKey: this.apiKey,
|
|
1665
|
+
mapType: this.mapType,
|
|
1666
|
+
language: this.language,
|
|
1667
|
+
region: this.region,
|
|
1668
|
+
tileApiUrl: this.tileApiUrl,
|
|
1669
|
+
includeApiKey: this.includeApiKey,
|
|
1670
|
+
headers: this.headers,
|
|
1671
|
+
fetchImpl: this.fetchImpl
|
|
1672
|
+
});
|
|
1673
|
+
this.sessionToken = data.session;
|
|
1674
|
+
const expiry = Number(data.expiry);
|
|
1675
|
+
this.sessionExpiry = Number.isFinite(expiry) ? expiry : void 0;
|
|
1676
|
+
if (typeof data.tileWidth === "number") {
|
|
1677
|
+
this.tileSize = data.tileWidth;
|
|
1678
|
+
}
|
|
1679
|
+
return this.sessionToken;
|
|
1680
|
+
}
|
|
1681
|
+
async getImage(polygon2, bands, expression, zoomLevel, requiresSquare = false, stitch = true) {
|
|
1682
|
+
await this.ensureSession();
|
|
1683
|
+
return super.getImage(
|
|
1684
|
+
polygon2,
|
|
1685
|
+
bands,
|
|
1686
|
+
expression,
|
|
1687
|
+
zoomLevel,
|
|
1688
|
+
requiresSquare,
|
|
1689
|
+
stitch
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
getTileUrlFromTileCoords(tileCoords, instance) {
|
|
1693
|
+
if (!instance.sessionToken) {
|
|
1694
|
+
throw new Error(
|
|
1695
|
+
"Google Maps session is not ready. Call ensureSession() or getImage() first."
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
const [x, y, z] = tileCoords;
|
|
1699
|
+
return buildGoogleTileUrl(
|
|
1700
|
+
z,
|
|
1701
|
+
x,
|
|
1702
|
+
y,
|
|
1703
|
+
instance.sessionToken,
|
|
1704
|
+
instance.apiKey,
|
|
1705
|
+
instance.tileApiUrl,
|
|
1706
|
+
instance.includeApiKey
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1383
1710
|
class BaseModel {
|
|
1384
1711
|
constructor(model_id, providerParams, modelParams) {
|
|
1385
1712
|
__publicField(this, "providerParams");
|
|
@@ -1449,6 +1776,33 @@ class BaseModel {
|
|
|
1449
1776
|
extraParams: this.providerParams.extraParams
|
|
1450
1777
|
});
|
|
1451
1778
|
break;
|
|
1779
|
+
case "oam":
|
|
1780
|
+
this.dataProvider = new Oam({
|
|
1781
|
+
stacUrl: this.providerParams.stacUrl,
|
|
1782
|
+
rasterUrl: this.providerParams.rasterUrl,
|
|
1783
|
+
collection: this.providerParams.collection,
|
|
1784
|
+
asset: this.providerParams.asset,
|
|
1785
|
+
itemId: this.providerParams.itemId,
|
|
1786
|
+
mosaic: this.providerParams.mosaic,
|
|
1787
|
+
attribution: this.providerParams.attribution,
|
|
1788
|
+
tileSize: this.providerParams.tileSize,
|
|
1789
|
+
headers: this.providerParams.headers
|
|
1790
|
+
});
|
|
1791
|
+
break;
|
|
1792
|
+
case "google":
|
|
1793
|
+
this.dataProvider = new GoogleMaps({
|
|
1794
|
+
apiKey: this.providerParams.apiKey,
|
|
1795
|
+
mapType: this.providerParams.mapType,
|
|
1796
|
+
language: this.providerParams.language,
|
|
1797
|
+
region: this.providerParams.region,
|
|
1798
|
+
sessionToken: this.providerParams.sessionToken,
|
|
1799
|
+
tileApiUrl: this.providerParams.tileApiUrl,
|
|
1800
|
+
includeApiKey: this.providerParams.includeApiKey,
|
|
1801
|
+
attribution: this.providerParams.attribution,
|
|
1802
|
+
tileSize: this.providerParams.tileSize,
|
|
1803
|
+
headers: this.providerParams.headers
|
|
1804
|
+
});
|
|
1805
|
+
break;
|
|
1452
1806
|
case "sentinel":
|
|
1453
1807
|
throw new Error("Sentinel provider not implemented yet");
|
|
1454
1808
|
default:
|
|
@@ -3685,7 +4039,147 @@ const BuildingSegmentationFactory = {
|
|
|
3685
4039
|
return create(params, resolvedId, modelParams);
|
|
3686
4040
|
}
|
|
3687
4041
|
};
|
|
4042
|
+
const GPS_GALLERY_URL = "https://huggingface.co/Xenova/geoclip-large-patch14/resolve/main/gps_gallery/coordinates_100K.json";
|
|
4043
|
+
const DEFAULT_TOP_K = 5;
|
|
4044
|
+
const COORDINATE_BATCH_SIZE = 512;
|
|
4045
|
+
const LOGIT_SCALE = Math.exp(3.681034803390503);
|
|
4046
|
+
const _ImageGeolocation = class _ImageGeolocation extends BaseModel {
|
|
4047
|
+
constructor(modelId, providerParams, modelParams) {
|
|
4048
|
+
super(modelId, providerParams, modelParams);
|
|
4049
|
+
__publicField(this, "visionModel");
|
|
4050
|
+
__publicField(this, "locationModel");
|
|
4051
|
+
__publicField(this, "processor");
|
|
4052
|
+
__publicField(this, "gpsGallery");
|
|
4053
|
+
}
|
|
4054
|
+
static async getInstance(modelId, providerParams, modelParams) {
|
|
4055
|
+
if (!_ImageGeolocation.instance || parametersChanged(
|
|
4056
|
+
_ImageGeolocation.instance,
|
|
4057
|
+
modelId,
|
|
4058
|
+
providerParams,
|
|
4059
|
+
modelParams
|
|
4060
|
+
)) {
|
|
4061
|
+
_ImageGeolocation.instance = new _ImageGeolocation(
|
|
4062
|
+
modelId,
|
|
4063
|
+
providerParams,
|
|
4064
|
+
modelParams
|
|
4065
|
+
);
|
|
4066
|
+
await _ImageGeolocation.instance.initialize();
|
|
4067
|
+
}
|
|
4068
|
+
return { instance: _ImageGeolocation.instance };
|
|
4069
|
+
}
|
|
4070
|
+
async initializeModel() {
|
|
4071
|
+
var _a;
|
|
4072
|
+
this.visionModel = await AutoModel.from_pretrained(this.model_id, {
|
|
4073
|
+
...this.modelParams,
|
|
4074
|
+
model_file_name: "vision_model"
|
|
4075
|
+
});
|
|
4076
|
+
this.locationModel = await AutoModel.from_pretrained(this.model_id, {
|
|
4077
|
+
model_file_name: "location_model",
|
|
4078
|
+
dtype: "fp32",
|
|
4079
|
+
device: (_a = this.modelParams) == null ? void 0 : _a.device
|
|
4080
|
+
});
|
|
4081
|
+
this.processor = await AutoProcessor.from_pretrained(
|
|
4082
|
+
"openai/clip-vit-large-patch14"
|
|
4083
|
+
);
|
|
4084
|
+
}
|
|
4085
|
+
async getGpsGallery() {
|
|
4086
|
+
if (!this.gpsGallery) {
|
|
4087
|
+
const response = await fetch(GPS_GALLERY_URL);
|
|
4088
|
+
if (!response.ok)
|
|
4089
|
+
throw new Error(
|
|
4090
|
+
`Unable to load GeoCLIP's GPS gallery (${response.status})`
|
|
4091
|
+
);
|
|
4092
|
+
this.gpsGallery = await response.json();
|
|
4093
|
+
}
|
|
4094
|
+
return this.gpsGallery;
|
|
4095
|
+
}
|
|
4096
|
+
async inference(params) {
|
|
4097
|
+
var _a, _b, _c;
|
|
4098
|
+
const { polygon: polygon2, image, topK = DEFAULT_TOP_K } = params.inputs;
|
|
4099
|
+
if (!(image instanceof Blob) && (!polygon2 || !polygon2.geometry || polygon2.geometry.type !== "Polygon")) {
|
|
4100
|
+
throw new Error(
|
|
4101
|
+
"Provide an uploaded image or a valid GeoJSON Polygon feature for image geolocation"
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
4104
|
+
if (!this.initialized) await this.initialize();
|
|
4105
|
+
if (!this.visionModel || !this.locationModel || !this.processor)
|
|
4106
|
+
throw new Error("GeoCLIP model was not initialized");
|
|
4107
|
+
const geoRawImage = image instanceof Blob ? void 0 : await this.polygonToImage(
|
|
4108
|
+
polygon2,
|
|
4109
|
+
(_a = params.mapSourceParams) == null ? void 0 : _a.zoomLevel,
|
|
4110
|
+
(_b = params.mapSourceParams) == null ? void 0 : _b.bands,
|
|
4111
|
+
(_c = params.mapSourceParams) == null ? void 0 : _c.expression
|
|
4112
|
+
);
|
|
4113
|
+
const inputImage = image instanceof Blob ? await RawImage.fromBlob(image) : geoRawImage;
|
|
4114
|
+
const visionOutput = await this.visionModel(
|
|
4115
|
+
await this.processor(inputImage)
|
|
4116
|
+
);
|
|
4117
|
+
const imageEmbeds = visionOutput.image_embeds;
|
|
4118
|
+
if (!imageEmbeds)
|
|
4119
|
+
throw new Error("GeoCLIP vision model did not return image embeddings");
|
|
4120
|
+
const normalizedImage = imageEmbeds.normalize().data;
|
|
4121
|
+
const gallery = await this.getGpsGallery();
|
|
4122
|
+
const candidates = [];
|
|
4123
|
+
for (let offset = 0; offset < gallery.length; offset += COORDINATE_BATCH_SIZE) {
|
|
4124
|
+
const coordinates = gallery.slice(offset, offset + COORDINATE_BATCH_SIZE);
|
|
4125
|
+
const locationOutput = await this.locationModel({
|
|
4126
|
+
location: new Tensor("float32", coordinates.flat(), [
|
|
4127
|
+
coordinates.length,
|
|
4128
|
+
2
|
|
4129
|
+
])
|
|
4130
|
+
});
|
|
4131
|
+
const locationEmbeds = locationOutput.location_embeds;
|
|
4132
|
+
if (!locationEmbeds)
|
|
4133
|
+
throw new Error(
|
|
4134
|
+
"GeoCLIP location model did not return location embeddings"
|
|
4135
|
+
);
|
|
4136
|
+
const normalizedLocations = locationEmbeds.normalize().data;
|
|
4137
|
+
for (let coordinateIndex = 0; coordinateIndex < coordinates.length; coordinateIndex++) {
|
|
4138
|
+
let dotProduct = 0;
|
|
4139
|
+
for (let dimension = 0; dimension < normalizedImage.length; dimension++) {
|
|
4140
|
+
dotProduct += normalizedImage[dimension] * normalizedLocations[coordinateIndex * normalizedImage.length + dimension];
|
|
4141
|
+
}
|
|
4142
|
+
candidates.push({
|
|
4143
|
+
index: offset + coordinateIndex,
|
|
4144
|
+
gps: coordinates[coordinateIndex],
|
|
4145
|
+
score: LOGIT_SCALE * dotProduct
|
|
4146
|
+
});
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
const maxScore = candidates.reduce(
|
|
4150
|
+
(maximum, candidate) => Math.max(maximum, candidate.score),
|
|
4151
|
+
Number.NEGATIVE_INFINITY
|
|
4152
|
+
);
|
|
4153
|
+
const scoreTotal = candidates.reduce(
|
|
4154
|
+
(total, candidate) => total + Math.exp(candidate.score - maxScore),
|
|
4155
|
+
0
|
|
4156
|
+
);
|
|
4157
|
+
const predictions = candidates.map((candidate) => ({
|
|
4158
|
+
...candidate,
|
|
4159
|
+
score: Math.exp(candidate.score - maxScore) / scoreTotal
|
|
4160
|
+
})).sort((a, b) => b.score - a.score).slice(0, Math.max(1, Math.floor(Number(topK) || DEFAULT_TOP_K)));
|
|
4161
|
+
return {
|
|
4162
|
+
predictions,
|
|
4163
|
+
geoRawImage,
|
|
4164
|
+
metadata: { modelId: this.model_id, gallerySize: gallery.length }
|
|
4165
|
+
};
|
|
4166
|
+
}
|
|
4167
|
+
};
|
|
4168
|
+
__publicField(_ImageGeolocation, "instance", null);
|
|
4169
|
+
let ImageGeolocation = _ImageGeolocation;
|
|
3688
4170
|
const modelRegistry = [
|
|
4171
|
+
{
|
|
4172
|
+
task: "image-geolocation",
|
|
4173
|
+
library: "@huggingface/transformers",
|
|
4174
|
+
description: "Estimates where an image was taken by matching visual features against GeoCLIP's worldwide GPS gallery.",
|
|
4175
|
+
examples: [
|
|
4176
|
+
"Estimate where this landscape image was taken.",
|
|
4177
|
+
"Find likely locations for this aerial image.",
|
|
4178
|
+
"Geolocate this landmark photograph."
|
|
4179
|
+
],
|
|
4180
|
+
ioConfig: {},
|
|
4181
|
+
geobase_ai_pipeline: (params, modelId = "Xenova/geoclip-large-patch14", modelParams = { dtype: "q4" }) => ImageGeolocation.getInstance(modelId, params, modelParams)
|
|
4182
|
+
},
|
|
3689
4183
|
{
|
|
3690
4184
|
task: "zero-shot-object-detection",
|
|
3691
4185
|
library: "@huggingface/transformers",
|
package/index.d.ts
CHANGED
|
@@ -198,10 +198,112 @@ declare class Wms extends MapSource {
|
|
|
198
198
|
number
|
|
199
199
|
], instance: Wms): string;
|
|
200
200
|
}
|
|
201
|
+
export interface OamConfig {
|
|
202
|
+
/** STAC API root (default HOT Imagery STAC). */
|
|
203
|
+
stacUrl?: string;
|
|
204
|
+
/** TiTiler / raster API root. */
|
|
205
|
+
rasterUrl?: string;
|
|
206
|
+
/** STAC collection id. */
|
|
207
|
+
collection?: string;
|
|
208
|
+
/** Asset key used for RGB tiles (default `visual`). */
|
|
209
|
+
asset?: string;
|
|
210
|
+
/**
|
|
211
|
+
* Pin to a specific STAC item id. Skips search.
|
|
212
|
+
* Item tiles: `/collections/{collection}/items/{itemId}/tiles/...`
|
|
213
|
+
*/
|
|
214
|
+
itemId?: string;
|
|
215
|
+
/**
|
|
216
|
+
* When true, use the collection mosaic tile endpoint (no STAC search).
|
|
217
|
+
* When false (default) and `itemId` is unset, search STAC for the AOI and
|
|
218
|
+
* use the best matching item's tiles (fallback to mosaic if none found).
|
|
219
|
+
*/
|
|
220
|
+
mosaic?: boolean;
|
|
221
|
+
attribution?: string;
|
|
222
|
+
tileSize?: number;
|
|
223
|
+
headers?: Record<string, string>;
|
|
224
|
+
}
|
|
225
|
+
declare class Oam extends MapSource {
|
|
226
|
+
stacUrl: string;
|
|
227
|
+
rasterUrl: string;
|
|
228
|
+
collection: string;
|
|
229
|
+
asset: string;
|
|
230
|
+
itemId?: string;
|
|
231
|
+
mosaic: boolean;
|
|
232
|
+
attribution: string;
|
|
233
|
+
tileSize: number;
|
|
234
|
+
headers?: Record<string, string>;
|
|
235
|
+
/** Resolved XYZ template with `{z}/{x}/{y}` placeholders. */
|
|
236
|
+
tileUrlTemplate: string;
|
|
237
|
+
private resolvedForBboxKey?;
|
|
238
|
+
constructor(config?: OamConfig);
|
|
239
|
+
/**
|
|
240
|
+
* Resolve tile URL template for an AOI via STAC (unless mosaic/itemId pin).
|
|
241
|
+
*/
|
|
242
|
+
resolveTileTemplate(polygon: GeoJSON.Feature): Promise<string>;
|
|
243
|
+
getImage(polygon: any, bands?: number[], expression?: string, zoomLevel?: number, requiresSquare?: boolean, stitch?: boolean): Promise<GeoRawImage | GeoRawImage[][]>;
|
|
244
|
+
protected getTileUrlFromTileCoords(tileCoords: [
|
|
245
|
+
number,
|
|
246
|
+
number,
|
|
247
|
+
number
|
|
248
|
+
], instance: Oam): string;
|
|
249
|
+
}
|
|
250
|
+
export type GoogleMapType = "satellite" | "roadmap" | "terrain";
|
|
251
|
+
export interface GoogleConfig {
|
|
252
|
+
/** Google Maps Platform API key with Map Tiles API enabled. */
|
|
253
|
+
apiKey: string;
|
|
254
|
+
/** 2D map type for the session (default `satellite`). */
|
|
255
|
+
mapType?: GoogleMapType;
|
|
256
|
+
language?: string;
|
|
257
|
+
region?: string;
|
|
258
|
+
/**
|
|
259
|
+
* Optional pre-created session token. When omitted, the provider creates
|
|
260
|
+
* one via `POST /v1/createSession` and refreshes near expiry.
|
|
261
|
+
*/
|
|
262
|
+
sessionToken?: string;
|
|
263
|
+
/** Session/tile API root (default https://tile.googleapis.com). */
|
|
264
|
+
tileApiUrl?: string;
|
|
265
|
+
/**
|
|
266
|
+
* When false, omit `key=` from createSession/tile URLs (use with a same-origin
|
|
267
|
+
* proxy that injects the server-side API key). Default true.
|
|
268
|
+
*/
|
|
269
|
+
includeApiKey?: boolean;
|
|
270
|
+
attribution?: string;
|
|
271
|
+
tileSize?: number;
|
|
272
|
+
headers?: Record<string, string>;
|
|
273
|
+
/** Injected for tests. */
|
|
274
|
+
fetchImpl?: typeof fetch;
|
|
275
|
+
}
|
|
276
|
+
declare class GoogleMaps extends MapSource {
|
|
277
|
+
apiKey: string;
|
|
278
|
+
mapType: GoogleMapType;
|
|
279
|
+
language: string;
|
|
280
|
+
region: string;
|
|
281
|
+
tileApiUrl: string;
|
|
282
|
+
includeApiKey: boolean;
|
|
283
|
+
attribution: string;
|
|
284
|
+
tileSize: number;
|
|
285
|
+
headers?: Record<string, string>;
|
|
286
|
+
fetchImpl: typeof fetch;
|
|
287
|
+
sessionToken?: string;
|
|
288
|
+
/** Unix epoch seconds when the session expires. */
|
|
289
|
+
sessionExpiry?: number;
|
|
290
|
+
constructor(config: GoogleConfig);
|
|
291
|
+
private isSessionValid;
|
|
292
|
+
/**
|
|
293
|
+
* Ensure a valid Map Tiles session exists (create or refresh).
|
|
294
|
+
*/
|
|
295
|
+
ensureSession(): Promise<string>;
|
|
296
|
+
getImage(polygon: any, bands?: number[], expression?: string, zoomLevel?: number, requiresSquare?: boolean, stitch?: boolean): Promise<GeoRawImage | GeoRawImage[][]>;
|
|
297
|
+
protected getTileUrlFromTileCoords(tileCoords: [
|
|
298
|
+
number,
|
|
299
|
+
number,
|
|
300
|
+
number
|
|
301
|
+
], instance: GoogleMaps): string;
|
|
302
|
+
}
|
|
201
303
|
declare abstract class BaseModel {
|
|
202
304
|
protected static instance: BaseModel | null;
|
|
203
305
|
protected providerParams: ProviderParams;
|
|
204
|
-
protected dataProvider: Mapbox | Geobase | Esri | Tms | Wms | undefined;
|
|
306
|
+
protected dataProvider: Mapbox | Geobase | Esri | Tms | Wms | Oam | GoogleMaps | undefined;
|
|
205
307
|
protected model_id: string;
|
|
206
308
|
protected initialized: boolean;
|
|
207
309
|
protected modelParams?: PretrainedModelOptions;
|
|
@@ -522,6 +624,20 @@ declare class ChangeStarBuildingSegmentation extends BaseDenseSegmentationModel
|
|
|
522
624
|
instance: ChangeStarBuildingSegmentation;
|
|
523
625
|
}>;
|
|
524
626
|
}
|
|
627
|
+
declare class ImageGeolocation extends BaseModel {
|
|
628
|
+
protected static instance: ImageGeolocation | null;
|
|
629
|
+
private visionModel;
|
|
630
|
+
private locationModel;
|
|
631
|
+
private processor;
|
|
632
|
+
private gpsGallery;
|
|
633
|
+
private constructor();
|
|
634
|
+
static getInstance(modelId: string, providerParams: ProviderParams, modelParams?: PretrainedModelOptions): Promise<{
|
|
635
|
+
instance: ImageGeolocation;
|
|
636
|
+
}>;
|
|
637
|
+
protected initializeModel(): Promise<void>;
|
|
638
|
+
private getGpsGallery;
|
|
639
|
+
inference(params: InferenceParams): Promise<ImageGeolocationResults>;
|
|
640
|
+
}
|
|
525
641
|
export type MapboxParams = {
|
|
526
642
|
provider: "mapbox";
|
|
527
643
|
apiKey: string;
|
|
@@ -568,8 +684,47 @@ export type WmsParams = {
|
|
|
568
684
|
headers?: Record<string, string>;
|
|
569
685
|
extraParams?: Record<string, string>;
|
|
570
686
|
};
|
|
687
|
+
export type OamParams = {
|
|
688
|
+
provider: "oam";
|
|
689
|
+
/** STAC API root (default https://api.imagery.hotosm.org/stac). */
|
|
690
|
+
stacUrl?: string;
|
|
691
|
+
/** Raster/TiTiler API root (default https://api.imagery.hotosm.org/raster). */
|
|
692
|
+
rasterUrl?: string;
|
|
693
|
+
/** STAC collection id (default openaerialmap). */
|
|
694
|
+
collection?: string;
|
|
695
|
+
/** Asset key for RGB tiles (default visual). */
|
|
696
|
+
asset?: string;
|
|
697
|
+
/** Pin to a specific STAC item id (skips search). */
|
|
698
|
+
itemId?: string;
|
|
699
|
+
/** Use collection mosaic tiles instead of per-item STAC search. */
|
|
700
|
+
mosaic?: boolean;
|
|
701
|
+
attribution?: string;
|
|
702
|
+
tileSize?: number;
|
|
703
|
+
headers?: Record<string, string>;
|
|
704
|
+
};
|
|
705
|
+
export type GoogleMapsParams = {
|
|
706
|
+
provider: "google";
|
|
707
|
+
/** Google Maps Platform API key with Map Tiles API enabled. */
|
|
708
|
+
apiKey: string;
|
|
709
|
+
/** 2D map type for the session (default satellite). */
|
|
710
|
+
mapType?: "satellite" | "roadmap" | "terrain";
|
|
711
|
+
language?: string;
|
|
712
|
+
region?: string;
|
|
713
|
+
/** Optional pre-created session token (skips createSession until expiry). */
|
|
714
|
+
sessionToken?: string;
|
|
715
|
+
/** Override API root (e.g. same-origin proxy). Default https://tile.googleapis.com */
|
|
716
|
+
tileApiUrl?: string;
|
|
717
|
+
/**
|
|
718
|
+
* When false, omit `key=` from requests (proxy injects the server key).
|
|
719
|
+
* Default true.
|
|
720
|
+
*/
|
|
721
|
+
includeApiKey?: boolean;
|
|
722
|
+
attribution?: string;
|
|
723
|
+
tileSize?: number;
|
|
724
|
+
headers?: Record<string, string>;
|
|
725
|
+
};
|
|
571
726
|
export interface InferenceInputs {
|
|
572
|
-
polygon
|
|
727
|
+
polygon?: GeoJSON.Feature;
|
|
573
728
|
classLabel?: string;
|
|
574
729
|
[key: string]: unknown;
|
|
575
730
|
}
|
|
@@ -586,10 +741,10 @@ export interface InferenceParams {
|
|
|
586
741
|
postProcessingParams?: PostProcessingParams;
|
|
587
742
|
mapSourceParams?: MapSourceParams;
|
|
588
743
|
}
|
|
589
|
-
export type ProviderParams = MapboxParams | SentinelParams | GeobaseParams | EsriParams | TmsParams | WmsParams;
|
|
590
|
-
export type HuggingFaceModelTask = "mask-generation" | "zero-shot-object-detection" | "zero-shot-image-classification" | "object-detection" | "oriented-object-detection" | "image-feature-extraction";
|
|
744
|
+
export type ProviderParams = MapboxParams | SentinelParams | GeobaseParams | EsriParams | TmsParams | WmsParams | OamParams | GoogleMapsParams;
|
|
745
|
+
export type HuggingFaceModelTask = "mask-generation" | "zero-shot-object-detection" | "zero-shot-image-classification" | "object-detection" | "oriented-object-detection" | "image-feature-extraction" | "image-geolocation";
|
|
591
746
|
export type GeobaseAiModelTask = "damage-assessment" | "vegetation-classification" | "land-cover-classification" | "land-use-classification" | "land-cover-change-detection" | "land-use-change-detection" | "solar-panel-detection" | "ship-detection" | "car-detection" | "wetland-segmentation" | "building-detection" | "oil-storage-tank-detection" | "building-footprint-segmentation";
|
|
592
|
-
export type ModelInstance = MaskGeneration | ZeroShotObjectDetection | ObjectDetection | OrientedObjectDetection | LandCoverClassification | SolarPanelDetection | ShipDetection | CarDetection | WetLandSegmentation | BuildingDetection | ChangeStarBuildingSegmentation | OilStorageTankDetection | BuildingFootPrintSegmentation | ImageFeatureExtraction;
|
|
747
|
+
export type ModelInstance = MaskGeneration | ZeroShotObjectDetection | ObjectDetection | OrientedObjectDetection | LandCoverClassification | SolarPanelDetection | ShipDetection | CarDetection | WetLandSegmentation | BuildingDetection | ChangeStarBuildingSegmentation | OilStorageTankDetection | BuildingFootPrintSegmentation | ImageFeatureExtraction | ImageGeolocation;
|
|
593
748
|
export type ModelConfig = {
|
|
594
749
|
task: HuggingFaceModelTask | GeobaseAiModelTask;
|
|
595
750
|
library: string;
|
|
@@ -627,6 +782,21 @@ export interface ImageFeatureExtractionResults {
|
|
|
627
782
|
modelId: string;
|
|
628
783
|
};
|
|
629
784
|
}
|
|
785
|
+
export interface ImageGeolocationResults {
|
|
786
|
+
predictions: Array<{
|
|
787
|
+
index: number;
|
|
788
|
+
gps: [
|
|
789
|
+
number,
|
|
790
|
+
number
|
|
791
|
+
];
|
|
792
|
+
score: number;
|
|
793
|
+
}>;
|
|
794
|
+
geoRawImage?: GeoRawImage;
|
|
795
|
+
metadata: {
|
|
796
|
+
modelId: string;
|
|
797
|
+
gallerySize: number;
|
|
798
|
+
};
|
|
799
|
+
}
|
|
630
800
|
export interface ChainInstance {
|
|
631
801
|
inference: (inputs: InferenceParams) => Promise<ObjectDetectionResults | SegmentationResults>;
|
|
632
802
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "geoai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "A JavaScript library for running Geo AI models in frontend applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"geoai",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"test": "vitest",
|
|
64
64
|
"test:coverage": "vitest --coverage",
|
|
65
65
|
"test:build": "pnpm run build && vitest run test/build.test.ts",
|
|
66
|
+
"docs:llms": "node ./docs/scripts/generate-llms-txt.mjs",
|
|
66
67
|
"lint:scripts": "eslint . --ext .ts",
|
|
67
68
|
"lint:styles": "stylelint ./**/*.{css,scss}",
|
|
68
69
|
"format:scripts": "prettier . --write",
|