@globalfishingwatch/skills 0.0.1
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/README.md +89 -0
- package/dist/decode-url/SKILL.md +51 -0
- package/dist/decode-url/index.js +20339 -0
- package/dist/decode-url/scripts/decode-url.mjs +18 -0
- package/dist/decode-url/scripts/register-gfw-resolver.mjs +24 -0
- package/dist/encode-url/MAINTENANCE.md +53 -0
- package/dist/encode-url/SKILL.md +135 -0
- package/dist/encode-url/index.js +20211 -0
- package/dist/encode-url/references/areas.json +1401 -0
- package/dist/encode-url/references/examples-conversations.md +113 -0
- package/dist/encode-url/references/examples.md +234 -0
- package/dist/encode-url/references/filters.md +67 -0
- package/dist/encode-url/references/highlighted-workspaces.md +65 -0
- package/dist/encode-url/references/layers.md +96 -0
- package/dist/encode-url/references/ports.json +27507 -0
- package/dist/encode-url/references/query-params.md +114 -0
- package/dist/encode-url/references/routes.md +24 -0
- package/dist/encode-url/scripts/encode-url.mjs +26 -0
- package/dist/encode-url/scripts/register-gfw-resolver.mjs +24 -0
- package/dist/index.js +20514 -0
- package/dist/package.json +35 -0
- package/package.json +35 -0
- package/src/decode-url/SKILL.md +51 -0
- package/src/decode-url/decode.ts +87 -0
- package/src/decode-url/index.ts +4 -0
- package/src/decode-url/scripts/decode-url.mjs +18 -0
- package/src/decode-url/scripts/register-gfw-resolver.mjs +24 -0
- package/src/decode-url/workspaces.ts +34 -0
- package/src/encode-url/MAINTENANCE.md +53 -0
- package/src/encode-url/SKILL.md +135 -0
- package/src/encode-url/config.ts +8 -0
- package/src/encode-url/dictionary.ts +305 -0
- package/src/encode-url/encode.ts +79 -0
- package/src/encode-url/index.ts +4 -0
- package/src/encode-url/references/areas.json +1401 -0
- package/src/encode-url/references/examples-conversations.md +113 -0
- package/src/encode-url/references/examples.md +234 -0
- package/src/encode-url/references/filters.md +67 -0
- package/src/encode-url/references/highlighted-workspaces.md +65 -0
- package/src/encode-url/references/layers.md +96 -0
- package/src/encode-url/references/ports.json +27507 -0
- package/src/encode-url/references/query-params.md +114 -0
- package/src/encode-url/references/routes.md +24 -0
- package/src/encode-url/routes.ts +166 -0
- package/src/encode-url/scripts/encode-url.mjs +26 -0
- package/src/encode-url/scripts/register-gfw-resolver.mjs +24 -0
- package/src/index.ts +6 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@globalfishingwatch/skills",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Agent skills for the GFW map: encode/decode app URLs for chatbot navigation",
|
|
5
|
+
"homepage": "https://github.com/GlobalFishingWatch/frontend#readme",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./encode-url": {
|
|
14
|
+
"types": "./src/encode-url/index.ts",
|
|
15
|
+
"default": "./dist/encode-url/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./decode-url": {
|
|
18
|
+
"types": "./src/decode-url/index.ts",
|
|
19
|
+
"default": "./dist/decode-url/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./src/index.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/GlobalFishingWatch/frontend.git"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"tslib": "2.x"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@globalfishingwatch/skills",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Agent skills for the GFW map: encode/decode app URLs for chatbot navigation",
|
|
5
|
+
"homepage": "https://github.com/GlobalFishingWatch/frontend#readme",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./encode-url": {
|
|
14
|
+
"types": "./src/encode-url/index.ts",
|
|
15
|
+
"default": "./dist/encode-url/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./decode-url": {
|
|
18
|
+
"types": "./src/decode-url/index.ts",
|
|
19
|
+
"default": "./dist/decode-url/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./src/index.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/GlobalFishingWatch/frontend.git"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"tslib": "2.x"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: decode-url
|
|
3
|
+
description: Decode a GFW map URL into structured context of what the user is currently seeing — route (map/report/vessel/search), visible layers with filters, time range and viewport. Use whenever you receive the user's current URL and need to understand or describe their view, or to modify it (decode, edit the raw state, re-encode with the encode-url skill).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Decode GFW map URL
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
node scripts/decode-url.mjs '<url or path>'
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Accepts a full URL (`https://globalfishingwatch.org/map/...`) or just the path. Output:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"route": {
|
|
17
|
+
"type": "report",
|
|
18
|
+
"category": "fishing-activity",
|
|
19
|
+
"workspaceId": "default-public",
|
|
20
|
+
"datasetId": "public-eez-areas",
|
|
21
|
+
"areaId": "5682"
|
|
22
|
+
},
|
|
23
|
+
"workspaceName": "Default public workspace",
|
|
24
|
+
"timeRange": { "start": "2025-08-01T00:00:00.000Z", "end": "2026-08-01T00:00:00.000Z" },
|
|
25
|
+
"viewport": { "latitude": 40.7, "longitude": 12.4, "zoom": 4.7 },
|
|
26
|
+
"timebarVisualisation": "events",
|
|
27
|
+
"layers": [
|
|
28
|
+
{
|
|
29
|
+
"id": "ais",
|
|
30
|
+
"name": "Apparent fishing effort (AIS)",
|
|
31
|
+
"category": "activity",
|
|
32
|
+
"visible": true,
|
|
33
|
+
"filters": { "geartype": ["trawlers"] }
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"report": { "reportBufferValue": 50 },
|
|
37
|
+
"vessel": { "vesselSelfReportedId": "..." },
|
|
38
|
+
"search": { "query": "lake aurora" },
|
|
39
|
+
"raw": {}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
How to narrate it:
|
|
44
|
+
|
|
45
|
+
- `route.type` says where the user is: `workspace` (map browsing), `report` (area report — `areaId` within `datasetId`, e.g. `public-eez-areas`; no `datasetId`/`areaId` = global report), `ports-report`, `vessel` (vessel profile), `vessel-search`, `vessel-group-report`, `user`, `workspaces-list`.
|
|
46
|
+
- `workspaceName` (when present) names a curated workspace: Marine Manager MPAs (`category: "marine-manager"`, e.g. Palau, Galapagos and Hermandad) or global curated reports (`activity-report`, `detections-report`, `events-report`, Deep Sea Mining Watch). Lead with it — "you're looking at the Palau Marine Manager workspace" beats reciting layers.
|
|
47
|
+
- Describe only `visible: true` layers; mention hidden ones only if relevant. Layer `filters` use API values: `flag` is ISO3 (translate to country names), `matched: ["false"]` on detections means dark vessels (no AIS match), `geartype`/`vessel_type` are fleet filters.
|
|
48
|
+
- `timeRange` is the analyzed period; no `timeRange` means the app default (roughly the last year).
|
|
49
|
+
- `raw` is the full parsed workspace state — edit it and feed it back to the encode-url skill (`{"route": <route>, "state": <raw>}`) to produce a modified view that keeps everything else identical.
|
|
50
|
+
|
|
51
|
+
Requires node >= 23 (uses `module.registerHooks`). In the monorepo run `pnpm nx dist skills` once so `dist/` exists.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { BaseUrlWorkspace } from '@globalfishingwatch/dataviews-client'
|
|
2
|
+
import { parseWorkspace } from '@globalfishingwatch/dataviews-client'
|
|
3
|
+
|
|
4
|
+
import type { LayerCategory } from '../encode-url/dictionary'
|
|
5
|
+
import { getLayerInfo } from '../encode-url/dictionary'
|
|
6
|
+
import type { MapRoute } from '../encode-url/routes'
|
|
7
|
+
import { DEFAULT_BASENAME, matchRoutePath } from '../encode-url/routes'
|
|
8
|
+
|
|
9
|
+
import { HIGHLIGHTED_WORKSPACES } from './workspaces'
|
|
10
|
+
|
|
11
|
+
export type DecodedLayer = {
|
|
12
|
+
id: string
|
|
13
|
+
dataviewId?: string
|
|
14
|
+
name: string
|
|
15
|
+
category: LayerCategory
|
|
16
|
+
visible: boolean
|
|
17
|
+
filters?: Record<string, unknown>
|
|
18
|
+
color?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type DecodedMapUrl = {
|
|
22
|
+
route: MapRoute
|
|
23
|
+
/** Curated workspace name (marine-manager, global reports…) when the workspaceId is a known one */
|
|
24
|
+
workspaceName?: string
|
|
25
|
+
timeRange?: { start?: string; end?: string }
|
|
26
|
+
viewport?: { latitude?: number; longitude?: number; zoom?: number }
|
|
27
|
+
timebarVisualisation?: string
|
|
28
|
+
layers: DecodedLayer[]
|
|
29
|
+
report?: Record<string, unknown>
|
|
30
|
+
vessel?: Record<string, unknown>
|
|
31
|
+
search?: Record<string, unknown>
|
|
32
|
+
/** Full parsed workspace state, pass it back to encodeMapUrl to reproduce the url */
|
|
33
|
+
raw: BaseUrlWorkspace & Record<string, unknown>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pickByPrefix = (state: Record<string, unknown>, prefixes: string[]) => {
|
|
37
|
+
const entries = Object.entries(state).filter(([key]) =>
|
|
38
|
+
prefixes.some((prefix) => key.startsWith(prefix))
|
|
39
|
+
)
|
|
40
|
+
return entries.length ? Object.fromEntries(entries) : undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const decodeMapUrl = (
|
|
44
|
+
url: string,
|
|
45
|
+
{ basename = DEFAULT_BASENAME }: { basename?: string } = {}
|
|
46
|
+
): DecodedMapUrl => {
|
|
47
|
+
const { pathname, search } = new URL(url, 'https://globalfishingwatch.org')
|
|
48
|
+
const appPathname = pathname.startsWith(basename) ? pathname.slice(basename.length) : pathname
|
|
49
|
+
const route = matchRoutePath(appPathname)
|
|
50
|
+
const state = parseWorkspace(search) as DecodedMapUrl['raw']
|
|
51
|
+
|
|
52
|
+
const layers: DecodedLayer[] = (state.dataviewInstances || []).flatMap((instance) => {
|
|
53
|
+
if (!instance?.id) return []
|
|
54
|
+
const { name, category } = getLayerInfo(instance.id)
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
id: instance.id,
|
|
58
|
+
...(instance.dataviewId && { dataviewId: String(instance.dataviewId) }),
|
|
59
|
+
name,
|
|
60
|
+
category,
|
|
61
|
+
visible: instance.config?.visible !== false,
|
|
62
|
+
...(instance.config?.filters && { filters: instance.config.filters }),
|
|
63
|
+
...(instance.config?.color && { color: instance.config.color }),
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const { start, end, latitude, longitude, zoom, timebarVisualisation } = state as Record<
|
|
69
|
+
string,
|
|
70
|
+
any
|
|
71
|
+
>
|
|
72
|
+
|
|
73
|
+
const workspaceName = route.workspaceId && HIGHLIGHTED_WORKSPACES[route.workspaceId]
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
route,
|
|
77
|
+
...(workspaceName && { workspaceName }),
|
|
78
|
+
...((start || end) && { timeRange: { start, end } }),
|
|
79
|
+
...(latitude !== undefined && { viewport: { latitude, longitude, zoom } }),
|
|
80
|
+
...(timebarVisualisation && { timebarVisualisation }),
|
|
81
|
+
layers,
|
|
82
|
+
report: pickByPrefix(state, ['report', 'portsReport']),
|
|
83
|
+
vessel: pickByPrefix(state, ['vessel', 'visibleEvents']),
|
|
84
|
+
search: pickByPrefix(state, ['query', 'searchOption', 'infoSource']),
|
|
85
|
+
raw: state,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import './register-gfw-resolver.mjs'
|
|
3
|
+
|
|
4
|
+
const url = process.argv[2]
|
|
5
|
+
if (!url) {
|
|
6
|
+
console.error(`Usage: node decode-url.mjs '<map url or path>'`)
|
|
7
|
+
process.exit(1)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { decodeMapUrl } = await import('@globalfishingwatch/skills/decode-url')
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const result = decodeMapUrl(url)
|
|
14
|
+
console.log(JSON.stringify(result, null, 2))
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error(error.message)
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { registerHooks } from 'node:module'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
// Each skill ships its own self-contained bundle next to it. Resolve it from either layout:
|
|
7
|
+
// - dist/<skill>/scripts (dist output, this skill copied standalone) -> ../index.js
|
|
8
|
+
// - <skill>/scripts (monorepo source, unbuilt) -> ../../dist/<skill>/index.js
|
|
9
|
+
const skillDir = fileURLToPath(new URL('..', import.meta.url))
|
|
10
|
+
const skillName = path.basename(skillDir)
|
|
11
|
+
const SPECIFIER = `@globalfishingwatch/skills/${skillName}`
|
|
12
|
+
const BUNDLE = [
|
|
13
|
+
path.join(skillDir, 'index.js'),
|
|
14
|
+
path.join(skillDir, '..', '..', 'dist', skillName, 'index.js'),
|
|
15
|
+
].find(existsSync)
|
|
16
|
+
|
|
17
|
+
registerHooks({
|
|
18
|
+
resolve(specifier, context, nextResolve) {
|
|
19
|
+
if (specifier === SPECIFIER && BUNDLE) {
|
|
20
|
+
return { url: pathToFileURL(BUNDLE).href, format: 'module', shortCircuit: true }
|
|
21
|
+
}
|
|
22
|
+
return nextResolve(specifier, context)
|
|
23
|
+
},
|
|
24
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DEFAULT_WORKSPACE_ID } from '@fishing-map/config'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Curated public workspace names, to describe what the user is looking at.
|
|
5
|
+
* Sources: apps/fishing-map/data/highlighted-workspaces/{marine-manager,reports}.ts
|
|
6
|
+
* + public/locales/source/workspaces.json
|
|
7
|
+
*/
|
|
8
|
+
export const HIGHLIGHTED_WORKSPACES: Record<string, string> = {
|
|
9
|
+
[DEFAULT_WORKSPACE_ID]: 'Default public workspace',
|
|
10
|
+
// Marine Manager
|
|
11
|
+
'ascension-public': 'Ascension Island',
|
|
12
|
+
'fiji-public': 'Fiji',
|
|
13
|
+
'guyana-public': 'Guyana',
|
|
14
|
+
'micronesia-public': 'Federated States of Micronesia',
|
|
15
|
+
'maldives-public': 'Maldives',
|
|
16
|
+
'niue-public': 'Niue',
|
|
17
|
+
'palau-public': 'Palau',
|
|
18
|
+
'tristan-public': 'Tristan da Cunha',
|
|
19
|
+
'mediterranean-public': 'Mediterranean and Black Sea',
|
|
20
|
+
'costa_rica-public': 'Costa Rica',
|
|
21
|
+
'colombia-public': 'Colombia',
|
|
22
|
+
'panama-public': 'Panama',
|
|
23
|
+
'cmar_core_mpas-public': 'CMAR core MPAs',
|
|
24
|
+
'galapagos_and_hermandad-public': 'Galapagos and Hermandad',
|
|
25
|
+
'rapanui-public': 'Rapa Nui',
|
|
26
|
+
'revillagigedo-public': 'Revillagigedo',
|
|
27
|
+
'revillagigedo_mexico-public': 'Revillagigedo (report)',
|
|
28
|
+
// Curated reports
|
|
29
|
+
'activity-report': 'Global Vessel Activity report',
|
|
30
|
+
'detections-report': 'Global Dark Vessel Detections report',
|
|
31
|
+
'events-report': 'Global Vessel Events report',
|
|
32
|
+
'deep-sea-mining-public': 'Deep Sea Mining Watch',
|
|
33
|
+
'deep_sea_mining-public': 'Deep Sea Mining Watch (report)',
|
|
34
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Maintaining the encode-url skill
|
|
2
|
+
|
|
3
|
+
Not loaded at skill runtime — this is the prompt/checklist for keeping the skill docs in sync with the app. Run it after changes to the fishing-map state types, URL encoding, or dataview/layer config.
|
|
4
|
+
|
|
5
|
+
## Update prompt
|
|
6
|
+
|
|
7
|
+
Paste (or point Claude at) the following:
|
|
8
|
+
|
|
9
|
+
> Sync the `encode-url` skill docs (`libs/skills/src/encode-url/`) with the app source. Requirements:
|
|
10
|
+
>
|
|
11
|
+
> 1. **Diff params against the documented types.** The four source-of-truth files carry JSDoc on every state field, all under `apps/fishing-map/`: `types/index.ts` (`WorkspaceState`/`AppState`/`QueryParams`), `features/reports/reports.types.ts`, `features/vessel/vessel.types.ts`, `features/search/search.types.ts`. Compare against `references/query-params.md`; add new intent-useful params, remove deleted ones.
|
|
12
|
+
> 2. **Diff every enumerated value.** The "Value sources (for update checks)" table in `libs/skills/src/encode-url/MAINTENANCE.md` maps each param to the exact symbol and file its values were copied from. Read each symbol; diff its values against the doc tables in `references/query-params.md`; fix drift. Add a row for any newly documented enum param.
|
|
13
|
+
> 3. **Coverage policy: curated, not exhaustive.** Document params a URL-building intent can actually use. Internal/auto-generated params stay out of the tables and go in the "never set" footnote near the top of `references/query-params.md` — that footnote is the authoritative list; keep it current instead of documenting those params.
|
|
14
|
+
> 4. **URL param names, not state field names.** A `state` param must match a key in `PARAMS_TO_ABBREVIATED` (`libs/dataviews-client/src/url-workspace/url-workspace.ts`) or serialize under its full name. Known trap: URL param `reportResultsPerPage` vs type field `reportVesselResultsPerPage`. Advanced search fields (`transmissionDateFrom/To`, from `ADVANCED_SEARCH_QUERY_FIELDS` in `libs/api-client/src/utils/search.ts`) are a different namespace from `firstTransmissionDate`/`lastTransmissionDate` (`fTD`/`lTD`).
|
|
15
|
+
> 5. **Layer/instance ids** come from `apps/fishing-map/config/src/workspaces.ts` constants and must resolve in the encoder dictionary `libs/skills/src/encode-url/dictionary.ts` (known trap: the FAO context instance is `context-layer-fao-areas`, not `context-layer-fao`).
|
|
16
|
+
> 6. **Defaults** come from `DEFAULT_WORKSPACE` (`apps/fishing-map/data/config.ts`) and `DEFAULT_REPORT_STATE` (`apps/fishing-map/features/reports/reports.config.ts`) — mark them `(default)` in the tables.
|
|
17
|
+
> 7. **Examples live in two files.** `references/examples.md` holds input recipes; `references/examples-conversations.md` holds conversation transcripts with expected output URLs. Update transcripts if encoder output changes.
|
|
18
|
+
> 8. **Verify, don't trust.** `pnpm nx dist skills`, then run `node scripts/encode-url.mjs` (node >= 23) with inputs exercising every param you touched; confirm expected abbreviations/pass-through names in the output path. Round-trip against the transcripts in examples-conversations.md. Finish with `pnpm prettier --write` on every markdown file touched.
|
|
19
|
+
|
|
20
|
+
## Value sources (for update checks)
|
|
21
|
+
|
|
22
|
+
Every enumerated value in `references/query-params.md` is copied from one of these symbols. To check for updates: read each symbol and diff its values against the tables in that file. This table lives here (not in the reference) to keep skill-runtime token cost down.
|
|
23
|
+
|
|
24
|
+
| Param(s) | Symbol | File |
|
|
25
|
+
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
|
|
26
|
+
| `timebarVisualisation` | `TimebarVisualisations` enum | `apps/fishing-map/types/index.ts` |
|
|
27
|
+
| `timebarGraph` | `TimebarGraphs` enum | `apps/fishing-map/types/index.ts` |
|
|
28
|
+
| `visibleEvents` | `EventTypes` enum (app toggles use `Gaps = 'gaps'`, not `Gap`) | `libs/api-types/src/events.ts` |
|
|
29
|
+
| `activityVisualizationMode`, `detectionsVisualizationMode` | `FOURWINGS_VISUALIZATION_MODES` | `libs/deck-layers/src/layers/fourwings/fourwings.config.ts` |
|
|
30
|
+
| `environmentVisualizationMode` | `HEATMAP_ID`, `HEATMAP_LOW_RES_ID` | `libs/deck-layers/src/layers/fourwings/fourwings.config.ts` |
|
|
31
|
+
| `vesselGroupsVisualizationMode` | `FOOTPRINT_ID`, `FOOTPRINT_HIGH_RES_ID` | `libs/deck-layers/src/layers/fourwings/fourwings.config.ts` |
|
|
32
|
+
| `vesselsColorBy` | `VesselsColorByProperty` | `libs/deck-layers/src/layers/vessel/vessel.config.ts` |
|
|
33
|
+
| `mapDrawing` | `DrawFeatureType` | `libs/deck-layers/src/layers/draw/DrawLayer.ts` |
|
|
34
|
+
| `colorRamp`/`color` palette | `FILL_COLOR_BAR_OPTIONS` | `libs/ui-components/src/color-bar/color-bar-options.ts` |
|
|
35
|
+
| `reportCategory` | `ReportCategory` enum | `apps/fishing-map/features/reports/reports.types.ts` |
|
|
36
|
+
| `reportActivitySubCategory`, `reportDetectionsSubCategory`, `reportVesselsSubCategory` | `ReportActivitySubCategory`, `ReportDetectionsSubCategory`, `ReportVesselsSubCategory` types (built on `DatasetSubCategory` from `libs/api-types/src/datasets.ts`) | `apps/fishing-map/features/reports/reports.types.ts` |
|
|
37
|
+
| `reportEventsSubCategory` | `EventType` (= `EventTypes` values) | `libs/api-types/src/events.ts` |
|
|
38
|
+
| `reportActivityGraph` | `REPORT_ACTIVITY_GRAPH_*` consts | `apps/fishing-map/features/reports/reports.config.ts` |
|
|
39
|
+
| `reportEventsGraph` | `REPORT_EVENTS_GRAPH_*` consts | `apps/fishing-map/features/reports/reports.config.ts` |
|
|
40
|
+
| `reportVesselGraph` | `REPORT_VESSELS_GRAPH_*` consts | `apps/fishing-map/features/reports/reports.config.ts` |
|
|
41
|
+
| `reportVesselOrderProperty`/`Direction` | `REPORT_VESSEL_ORDER_PROPERTIES`, `REPORT_VESSEL_ORDER_DIRECTIONS` | `apps/fishing-map/features/reports/reports.types.ts` |
|
|
42
|
+
| `reportBufferUnit`, `reportBufferOperation` | `BUFFER_UNITS`, `BUFFER_OPERATIONS` | `apps/fishing-map/types/index.ts` |
|
|
43
|
+
| `vesselSection`, `vesselArea`, `vesselRelated`, `vesselActivityMode` | `VESSEL_SECTIONS`, `VESSEL_AREA_SUBSECTIONS`, `VESSEL_RELATED_SUBSECTIONS`, `VESSEL_PROFILE_ACTIVITY_MODES` | `apps/fishing-map/features/vessel/vessel.types.ts` |
|
|
44
|
+
| advanced search fields | `ADVANCED_SEARCH_QUERY_FIELDS` | `libs/api-client/src/utils/search.ts` |
|
|
45
|
+
| `userTab` | `UserTab` enum | `apps/fishing-map/types/index.ts` |
|
|
46
|
+
| URL param abbreviations | `PARAMS_TO_ABBREVIATED` (params NOT listed there serialize under their full name) | `libs/dataviews-client/src/url-workspace/url-workspace.ts` |
|
|
47
|
+
|
|
48
|
+
## Doc structure invariants
|
|
49
|
+
|
|
50
|
+
- Each reference file's header names its source-of-truth files.
|
|
51
|
+
- The "Value sources" table above stays current — symbol + file, no line numbers (they rot). It lives in MAINTENANCE.md, not in the references, to avoid skill-runtime token cost.
|
|
52
|
+
- SKILL.md points agents at the typed JSDoc for params not covered in query-params.md.
|
|
53
|
+
- Format touched markdown with `pnpm prettier --write`.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: encode-url
|
|
3
|
+
description: Build a GFW map URL (and TanStack Router navigation config) that matches exactly what the fishing-map app generates. Use when the user wants to see, compare or review ocean activity (fishing effort, vessel presence, detections, encounters, loitering, port visits, environment layers), open a report over an area or port, open a vessel profile, or search vessels — and you need to produce the link or in-app navigation for it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Encode GFW map URL
|
|
7
|
+
|
|
8
|
+
Turn a navigation intent into `{ navigation, path }`:
|
|
9
|
+
|
|
10
|
+
- `navigation`: TanStack Router config (`to`, `params`, `search`) — use to navigate from inside the map app.
|
|
11
|
+
- `path`: path + encoded query string (e.g. `/map/fishing-activity/default-public?...`) — for external navigation prepend the origin `https://globalfishingwatch.org`.
|
|
12
|
+
|
|
13
|
+
Never hand-build the query string: params are abbreviated (`dataviewInstances`→`dvIn`, `config`→`cfg`, `visible`→`vis`…) and repeated values are tokenized (`~0`). The script does this with the app's own encoder.
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. **Pick the route type** from the user intent (details in [references/routes.md](references/routes.md)):
|
|
18
|
+
- browse/compare activity on the map → `workspace`
|
|
19
|
+
- a marine protected area / region with a curated workspace (Galapagos, Palau, Fiji, Mediterranean…) or a global curated report (activity, dark vessel detections, events, deep sea mining) → ids in [references/highlighted-workspaces.md](references/highlighted-workspaces.md) only when marine manager is mentioned
|
|
20
|
+
- aggregated report over an area (EEZ, FAO, RFMO) → `report` (needs `datasetId` + `areaId`)
|
|
21
|
+
|
|
22
|
+
- An area report must ALSO include the matching **context layer** as a visible instance so the area outline renders: `context-layer-eez` / `context-layer-fao-areas` / `context-layer-rfmo` / `context-layer-mpa` (pick by area type; dataviewId in [references/layers.md](references/layers.md)), `{ "config": { "visible": true } }`. Missing it drops the area boundary.
|
|
23
|
+
|
|
24
|
+
- Multiple areas in one report → pass `datasetId`/`areaId` as comma-joined lists of equal length (one datasetId per areaId), e.g. `"datasetId":"public-fao-major,public-fao-major","areaId":"41,87"` → path `/report/public-fao-major%2Cpublic-fao-major/41%2C87`. Adding an area to an existing report = append its dataset+id to both. Re-center viewport (`latitude`/`longitude`/`zoom`) on the combined area set rather than keeping the source values.
|
|
25
|
+
- port activity profile → `ports-report` (needs `portId`)
|
|
26
|
+
- vessel profile → `vessel` (needs `vesselId`)
|
|
27
|
+
- find a vessel by name/MMSI/IMO/owner → `vessel-search`
|
|
28
|
+
- user account / saved workspaces → `user`
|
|
29
|
+
|
|
30
|
+
- marine-manager workspaces index → static path `/map/marine-manager` (no state/query at all)
|
|
31
|
+
- If both a `workspace` (browse on the map) and a `report` (aggregated stats over an area) could serve the intent, return both results — the map view and the report — so the user picks.
|
|
32
|
+
|
|
33
|
+
2. **Pick layers** as `state.dataviewInstances` using the instance ids in [references/layers.md](references/layers.md). Rules:
|
|
34
|
+
- A layer the user wants: `{ "id": "<instance-id>", "config": { "visible": true, ... } }`.
|
|
35
|
+
- Default-workspace layers the user does NOT want (`ais`, `vms` are visible by default) must be included with `"config": { "visible": false }` — otherwise the app shows them anyway. This `ais`/`vms` default is for `default-public` ONLY — a curated workspace (see [highlighted-workspaces.md](references/highlighted-workspaces.md)) bakes in its own different default ids and must be checked there (or decoded live) before assuming.
|
|
36
|
+
- Generic "fishing" intent → show AIS fishing effort (`ais`) and hide `vms` with `"config": { "visible": false }` unless the user specifically wants VMS. Only keep `vms` visible when asked for VMS/national-fleet data.
|
|
37
|
+
- Layers not in the default workspace need `dataviewId` too (see layers.md). Versioned dataview slugs take the literal `{PIPE_DATASET_VERSION}` token (e.g. `sar-v-{PIPE_DATASET_VERSION}`) — the script resolves it from the `PIPE_DATASET_VERSION` env variable so URLs always target the latest dataset pipeline version.
|
|
38
|
+
- Multiple visible instances of the same category (e.g. two fishing-effort layers filtered per flag): give each a distinct color so they're visually separable. `color` and `colorRamp` are one choice — set both to a matching palette entry (ramp id + its paired hex, table in query-params.md); don't repeat a ramp within the category.
|
|
39
|
+
3. **Set filters** in `config.filters` — valid filter ids and enum values per dataset are in [references/filters.md](references/filters.md). Flags are ISO3 codes (`ESP`, `FRA`, `CHN`).
|
|
40
|
+
- Multiple flags → one instance per flag (each with its own `filters.flag` + distinct color/colorRamp per the rule above), so they compare side by side. Only combine flags into a single instance's `filters.flag` array when the user asks for the combined/total (e.g. "Spain and France together").
|
|
41
|
+
4. **Set the rest of the state** ([references/query-params.md](references/query-params.md)): `start`/`end` (ISO datetimes), `latitude`/`longitude`/`zoom`, and `timebarVisualisation` (`heatmap` for activity, `heatmapDetections` for detections, `events` for encounters/loitering/port visits).
|
|
42
|
+
|
|
43
|
+
- **Relative time periods**: compute `start`/`end` with plain date math from the current datetime (e.g. "last year" = now minus 1 year → now). The encoder snaps both to the map's interval resolution automatically
|
|
44
|
+
- **Report viewport = don't set it.** For an area report the tools is going to calculate it dinamycally.
|
|
45
|
+
|
|
46
|
+
5. **Look up ids when needed**: grep [references/areas.json](references/areas.json) for EEZ/FAO/RFMO area ids and [references/ports.json](references/ports.json) for port ids (large file — always grep by name, never read it whole).
|
|
47
|
+
- Region not in areas.json → no area `report` possible (needs a real `areaId`). Options: (a) a `workspace` framed by viewport — set `latitude`/`longitude`/`zoom` to your best estimate of the region's center; (b) a **global report** for whole-world stats. Global report = `route.type` `report` with NO `datasetId`/`areaId` (→ `/reports/default-public/report`); set viewport to world (`latitude: 0`, `longitude: 0`, `zoom: ~0.8`, not `0`). A global/whole-world report keeps ALL default-workspace layers VISIBLE (`ais`, `vms`, `presence`, `sar`, `sentinel2`, `viirs`, `encounters`, `loitering`, `port-visits`) — do not hide them and do not include only the one the user named. Use the global report when the user wants aggregated numbers but no specific area matches.
|
|
48
|
+
6. **Run the script**:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
node scripts/encode-url.mjs '{"route":{"type":"report","datasetId":"public-eez-areas","areaId":"5682"},"state":{...}}'
|
|
52
|
+
# or pipe: echo '<json>' | node scripts/encode-url.mjs
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Input shape:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"route": {
|
|
60
|
+
"type": "workspace|report|vessel|vessel-search|ports-report|vessel-group-report|user",
|
|
61
|
+
"category": "fishing-activity",
|
|
62
|
+
"workspaceId": "default-public",
|
|
63
|
+
"datasetId": "...",
|
|
64
|
+
"areaId": "...",
|
|
65
|
+
"vesselId": "...",
|
|
66
|
+
"portId": "...",
|
|
67
|
+
"vesselGroupId": "..."
|
|
68
|
+
},
|
|
69
|
+
"state": {
|
|
70
|
+
"dataviewInstances": [],
|
|
71
|
+
"start": "...",
|
|
72
|
+
"end": "...",
|
|
73
|
+
"latitude": 0,
|
|
74
|
+
"longitude": 0,
|
|
75
|
+
"zoom": 1,
|
|
76
|
+
"timebarVisualisation": "events"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`category`/`workspaceId` default to `fishing-activity`/`default-public` when omitted.
|
|
82
|
+
|
|
83
|
+
Worked examples of real intents → inputs: [references/examples.md](references/examples.md). Real conversation transcripts with expected output URLs: [references/examples-conversations.md](references/examples-conversations.md).
|
|
84
|
+
|
|
85
|
+
A state param not covered in query-params.md may still exist — the app state types carry full JSDoc: `apps/fishing-map/types/index.ts`, `features/reports/reports.types.ts`, `features/vessel/vessel.types.ts`, `features/search/search.types.ts`.
|
|
86
|
+
|
|
87
|
+
## Working from an existing map URL
|
|
88
|
+
|
|
89
|
+
When the input gives a **current map URL** (a follow-up like "and by flag?", "what about other gear types?", "are there non-fishing vessels?"), decode it into the full state first and treat that as the base. Make the **smallest change** that answers the intent, then re-run the encoder on the whole state. Output = input state minus nothing, plus/edited only the user's delta.
|
|
90
|
+
|
|
91
|
+
- **Preserve everything unmentioned, verbatim.** Params commonly dropped by accident that MUST carry over:
|
|
92
|
+
- viewport: `latitude`, `longitude`, `zoom` (keep the exact input values; don't recompute).
|
|
93
|
+
- time: `start`, `end` (change only when the user gives a new period).
|
|
94
|
+
- report buffer: `rBO`/`rBU`/`rBV` (`reportBufferOperation`/`reportBufferUnit`/`reportBufferValue`).
|
|
95
|
+
- temporal filters: `fTD`/`lTD` (even when empty — emit `fTD=&lTD=`).
|
|
96
|
+
- every existing `dataviewInstances` entry at its current visibility (id, category, dvId, color, colorRamp, filters). Do NOT hide, drop, reorder, or strip filters from a layer the user didn't ask to change.
|
|
97
|
+
- **Add, don't replace.** "Also show / are there also X" ADDS a new instance for X while leaving current layers visible — not toggle the current one off.
|
|
98
|
+
- **Minimal filter change, no enumeration.** "Other / the rest / remaining / non-<current>" relative to an applied filter means **clear/broaden that filter** (set it to `''` / remove it) on the existing layer. Never enumerate the complement as an explicit value list, and never fabricate a new layer to hold it.
|
|
99
|
+
|
|
100
|
+
- **A country/region constraint is an area report, not just a flag filter.** "only in <country>", "restricted to <region>" → convert the route to a `report` over that country's EEZ (grep [references/areas.json](references/areas.json) for its `datasetId`/`areaId`), keeping all existing layers/filters.
|
|
101
|
+
- **Re-encode, never string-edit.** Feed the full decoded+modified state through `scripts/encode-url.mjs`; hand-patching the query string leaves stale/duplicated params (e.g. a stale `zoom`) and skips normalization.
|
|
102
|
+
|
|
103
|
+
Requires node >= 23 (uses `module.registerHooks`). In the monorepo run `pnpm nx dist skills` once so `dist/` exists.
|
|
104
|
+
|
|
105
|
+
## Getting it right — priority rules (read first)
|
|
106
|
+
|
|
107
|
+
These exist because the same mistakes recur. They override any instinct to hand-build.
|
|
108
|
+
|
|
109
|
+
### 1. Preserve on follow-ups — and the global-report exception is narrow
|
|
110
|
+
|
|
111
|
+
- Carry over every unmentioned param verbatim: viewport, `start`/`end`, `rBO`/`rBU`/`rBV`, `fTD=`/`lTD=`, and every existing layer at its current visibility/filters.
|
|
112
|
+
|
|
113
|
+
### 2. Filter-refinement follow-ups modify the existing layer in place — do NOT add a new one
|
|
114
|
+
|
|
115
|
+
- Questions like "any passenger boats?", "squid jiggers?", "only trawlers?" refine an EXISTING layer: set that filter on the layer already present, keep its id/color/category. Do NOT create a second layer for the filtered subset.
|
|
116
|
+
- Reserve "add a new instance" for genuinely additive intents that introduce a DIFFERENT category or data source ("also show detections", "add SAR").
|
|
117
|
+
- "Other / the rest / remaining / non-<current>" = clear/broaden the existing filter to empty; never enumerate the complement.
|
|
118
|
+
|
|
119
|
+
### 3. Clearing a filter emits it empty — never drop the key
|
|
120
|
+
|
|
121
|
+
- To broaden/clear a filter, output it as empty (`geartype=`), not by omitting it. An omitted key scores as `None` and fails; the app expects the empty value. This is a persistent, repeated loss.
|
|
122
|
+
|
|
123
|
+
### 4. AIS fishing-effort carries a default `distance_from_port_km=3`
|
|
124
|
+
|
|
125
|
+
- Every AIS apparent-fishing-effort layer (`ais` and `fishing-effort-ais__*`) includes `filters.distance_from_port_km=3` unless the user overrides it. Include it even when the user only mentioned a gear/flag filter.
|
|
126
|
+
|
|
127
|
+
### 5. Multi-country fishing intent = WORKSPACE with one layer per flag, not a multi-area report
|
|
128
|
+
|
|
129
|
+
- "Fishing of Peru, Argentina, Brazil and Chile" → a `workspace` with one fishing-effort-ais layer per flag, each with `filters.flag` and a distinct color/colorRamp
|
|
130
|
+
|
|
131
|
+
### 6. Curated workspaces have their own defaults, and their own EEZ may have a national dataset
|
|
132
|
+
|
|
133
|
+
- Don't carry the `default-public` assumption (`ais`/`vms` visible by default) into a curated workspace — check its real default ids in [highlighted-workspaces.md](references/highlighted-workspaces.md) (or decode the bare workspace URL) and hide the ones the user doesn't want.
|
|
134
|
+
- If the workspace's EEZ has a national fishing-effort dataset (e.g. Ecuador for Galapagos, Brazil), prefer it over plain global `ais` for that flag/region — pattern in [layers.md](references/layers.md#national-fishing-effort-datasets).
|
|
135
|
+
- Set the viewport to the region's center for a curated `workspace` route (not `0,0`) — the "don't set viewport" rule only applies to `report` routes.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PIPE_DATASET_VERSION as DEFAULT_PIPE_DATASET_VERSION } from '@globalfishingwatch/datasets-client'
|
|
2
|
+
|
|
3
|
+
export const PIPE_DATASET_VERSION: string = DEFAULT_PIPE_DATASET_VERSION || '4'
|
|
4
|
+
|
|
5
|
+
export const PIPE_DATASET_VERSION_TOKEN = '{PIPE_DATASET_VERSION}'
|
|
6
|
+
|
|
7
|
+
export const resolveDataviewSlug = (dataviewId: string): string =>
|
|
8
|
+
dataviewId.replaceAll(PIPE_DATASET_VERSION_TOKEN, PIPE_DATASET_VERSION)
|