@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,114 @@
|
|
|
1
|
+
# GFW map query params (state)
|
|
2
|
+
|
|
3
|
+
Pass these unabbreviated in `state` — the encoder abbreviates them.
|
|
4
|
+
Source of truth (params documented with JSDoc — read these when a param is missing here):
|
|
5
|
+
`apps/fishing-map/types/index.ts` (`WorkspaceState`/`AppState`/`QueryParams`), `apps/fishing-map/features/reports/reports.types.ts` (`ReportState`), `apps/fishing-map/features/vessel/vessel.types.ts` (`VesselProfileState`), `apps/fishing-map/features/search/search.types.ts` (`VesselSearchState`).
|
|
6
|
+
Defaults live in `DEFAULT_WORKSPACE` (`apps/fishing-map/data/config.ts`) and `DEFAULT_REPORT_STATE` (`features/reports/reports.config.ts`).
|
|
7
|
+
|
|
8
|
+
## Core (all routes)
|
|
9
|
+
|
|
10
|
+
| Param | Type | Notes |
|
|
11
|
+
| ------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
12
|
+
| `latitude`, `longitude`, `zoom` | number | Viewport. zoom 0 = world, ~4-6 = country, ~12 = port |
|
|
13
|
+
| `start`, `end` | ISO datetime | Time range, e.g. `2026-07-01T00:00:00.000Z` |
|
|
14
|
+
| `dataviewInstances` | array | The layers (see layers.md) |
|
|
15
|
+
| `timebarVisualisation` | string | `heatmap` (activity) \| `heatmapDetections` \| `events` \| `vessel` \| `vesselGroup` \| `environment` |
|
|
16
|
+
| `visibleEvents` | array \| `all` \| `none` | Event types on vessel tracks: `fishing`, `encounter`, `port_visit`, `loitering`, `gaps` (default `all`) |
|
|
17
|
+
| `timebarGraph` | string | `speed` \| `elevation` \| `none` |
|
|
18
|
+
| `sidebarOpen` | boolean | |
|
|
19
|
+
| `readOnly` | boolean | Share links that shouldn't be edited |
|
|
20
|
+
| `daysFromLatest` | number | Rolling window ending at latest available day |
|
|
21
|
+
| `bivariateDataviews` | [id, id] | Two activity layers compared in one bivariate ramp |
|
|
22
|
+
| `mapAnnotations`, `mapRulers` | arrays | User drawings/measurements; toggle without removing via `mapAnnotationsVisible` / `mapRulersVisible` (booleans, default `true`) |
|
|
23
|
+
| `mapDrawing` | `polygons` \| `points` \| false | Enables the draw-on-map feature; `mapDrawingEditId` targets an existing drawn feature for edit |
|
|
24
|
+
| `activityVisualizationMode` | string | Render mode for activity layers: `heatmap` (default) \| `heatmap-high-res` \| `heatmap-low-res` \| `positions` |
|
|
25
|
+
| `detectionsVisualizationMode` | string | Same values as activity; for detections layers (default `heatmap`) |
|
|
26
|
+
| `environmentVisualizationMode` | string | `heatmap` \| `heatmap-low-res` (default) — no high res for environment layers |
|
|
27
|
+
| `vesselGroupsVisualizationMode` | string | `footprint` (default) \| `footprint-high-res` |
|
|
28
|
+
| `vesselsColorBy` | string | Property coloring vessel tracks/points: `track` \| `speed` \| `elevation` |
|
|
29
|
+
|
|
30
|
+
Internal/auto-generated params — never set them: `reportAreaBounds`, `skipColorDomainSampling`, `migramarLayer`, `includeRelatedIdentities`, `trackCorrectionId`, `sidePanelId`/`sidePanelSubcontentId`/`sidePanelContent`.
|
|
31
|
+
|
|
32
|
+
## dataviewInstances item
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"id": "ais", // instance id (layers.md)
|
|
37
|
+
"dataviewId": "apparent-fishing-effort-ais-v-{PIPE_DATASET_VERSION}", // only for layers added on top of defaults; version token resolved by the encoder
|
|
38
|
+
"config": {
|
|
39
|
+
"visible": true,
|
|
40
|
+
"color": "#9CA4FF", // set together with colorRamp — use that ramp's hex (see palette below)
|
|
41
|
+
"colorRamp": "lilac", // teal|orange|magenta|yellow|lilac|sky|green|red|salmon
|
|
42
|
+
"filters": { "flag": ["FRA"], "geartype": ["trawlers"] }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`color`/`colorRamp` are one choice, not two. Pick a ramp and set `color` to its paired hex:
|
|
48
|
+
|
|
49
|
+
| colorRamp | color | colorRamp | color | colorRamp | color |
|
|
50
|
+
| --------- | --------- | --------- | --------- | --------- | --------- |
|
|
51
|
+
| teal | `#00FFBC` | sky | `#00EEFF` | green | `#A6FF59` |
|
|
52
|
+
| lilac | `#9CA4FF` | red | `#FF6854` | orange | `#FFAA0D` |
|
|
53
|
+
| salmon | `#FFAE9B` | yellow | `#FFEA00` | magenta | `#FF64CE` |
|
|
54
|
+
|
|
55
|
+
To show the SAME dataset twice with different filters (e.g. Spanish vs French fishing), reuse the default instance (`ais`) for one and add a second instance with a unique id (`fishing-effort-ais__<timestamp>`) + `dataviewId`.
|
|
56
|
+
|
|
57
|
+
## Area report (`report` route)
|
|
58
|
+
|
|
59
|
+
| Param | Values |
|
|
60
|
+
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
61
|
+
| `reportCategory` | `activity` (default) \| `detections` \| `events` \| `environment` \| `others` \| `vessel-groups` |
|
|
62
|
+
| `reportActivitySubCategory` | `fishing` \| `presence` — sub-filter within an activity report |
|
|
63
|
+
| `reportDetectionsSubCategory` | `sar` \| `viirs` \| `sentinel-2` — sub-filter within a detections report |
|
|
64
|
+
| `reportEventsSubCategory` | event type within an events report: `encounter` (default) \| `loitering` \| `port_visit` \| `gap` |
|
|
65
|
+
| `reportVesselsSubCategory` | vessels-tab sub-tab: `flag` (default) \| `geartype` \| `vesselType` \| `source` \| `coverage` |
|
|
66
|
+
| `reportActivityGraph` | `evolution` (default) \| `beforeAfter` \| `periodComparison` \| `datasetComparison` |
|
|
67
|
+
| `reportTimeComparison` | `{ "start", "compareStart", "duration", "durationType": "days"\|"months" }` — for `beforeAfter`/`periodComparison` |
|
|
68
|
+
| `reportComparisonDataviewIds` | `{ "main", "compare" }` dataview ids — for `datasetComparison` |
|
|
69
|
+
| `reportVesselGraph` | `flag` (default) \| `geartype` \| `vesselType` |
|
|
70
|
+
| `reportBufferValue` / `reportBufferUnit` / `reportBufferOperation` | number / `nauticalmiles`\|`kilometers` / `dissolve`\|`difference` |
|
|
71
|
+
| `reportVesselFilter` | free-text filter on the vessels list — see syntax below |
|
|
72
|
+
| `reportVesselPage`, `reportResultsPerPage` | pagination (page is 0-based; per-page min 10, max 50, default 10) |
|
|
73
|
+
| `reportVesselOrderProperty` / `reportVesselOrderDirection` | sort: `shipname` (default) \| `flag` \| `shiptype` / `asc` (default) \| `desc` |
|
|
74
|
+
| `reportLoadVessels` | boolean, load vessel list immediately |
|
|
75
|
+
| `reportEventsGraph` | events-report chart: `evolution` (default) \| `byFlag` \| `byRFMO` \| `byFAO` \| `byEEZ` |
|
|
76
|
+
| `reportEventsPortsFilter`, `reportEventsPortsPage`, `reportEventsPortsResultsPerPage` | ports list controls in an events report (same semantics as the vessel-table ones) |
|
|
77
|
+
|
|
78
|
+
Note: the URL param is `reportResultsPerPage` even though the app state field is named `reportVesselResultsPerPage` — always use `reportResultsPerPage` in `state`.
|
|
79
|
+
|
|
80
|
+
`reportVesselFilter` syntax (also `reportEventsPortsFilter`): comma = search by multiple fields, `|` = OR, leading `-` = exclude. E.g. `flag:china, gear:trawlers`, `-spain`, `cargo|passenger`.
|
|
81
|
+
|
|
82
|
+
## Ports report (`ports-report` route)
|
|
83
|
+
|
|
84
|
+
`portsReportName` (e.g. `CAMARONES`), `portsReportCountry` (ISO3), `portsReportDatasetId` (`public-global-port-visits-events:v4.0`).
|
|
85
|
+
|
|
86
|
+
## Vessel profile (`vessel` route)
|
|
87
|
+
|
|
88
|
+
| Param | Values |
|
|
89
|
+
| ------------------------------------------- | --------------------------------------------------------------- |
|
|
90
|
+
| `vesselDatasetId` | `public-global-vessel-identity:v4.0` |
|
|
91
|
+
| `vesselSelfReportedId` / `vesselRegistryId` | vessel identity ids |
|
|
92
|
+
| `vesselIdentitySource` | `selfReportedInfo` \| `registryInfo` |
|
|
93
|
+
| `vesselSection` | `activity` \| `related_vessels` \| `areas` \| `insights` |
|
|
94
|
+
| `vesselArea` | `fao` \| `eez` \| `mpa` \| `rfmo` |
|
|
95
|
+
| `vesselRelated` | `encounters` \| `owners` — when `vesselSection=related_vessels` |
|
|
96
|
+
| `vesselActivityMode` | `voyage` \| `type` |
|
|
97
|
+
|
|
98
|
+
## Vessel search (`vessel-search` route)
|
|
99
|
+
|
|
100
|
+
| Param | Notes |
|
|
101
|
+
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `query` | free text: name, MMSI, IMO, callsign |
|
|
103
|
+
| `searchOption` | `basic` (default) \| `advanced` |
|
|
104
|
+
| `sources` | dataset ids restricting the search (advanced only) |
|
|
105
|
+
| advanced fields | `flag` (array ISO3), `owner`, `ssvid` (MMSI), `imo`, `callsign`, `geartypes`, `shiptypes`, `transmissionDateFrom`/`transmissionDateTo` |
|
|
106
|
+
| `infoSource` | identity source filter |
|
|
107
|
+
|
|
108
|
+
`transmissionDateFrom`/`transmissionDateTo` are advanced search FIELDS (dates, `YYYY-MM-DD`) — different namespace from the `firstTransmissionDate`/`lastTransmissionDate` URL params (`fTD`/`lTD`) that always appear (often empty) in search URLs.
|
|
109
|
+
|
|
110
|
+
## User page (`user` route)
|
|
111
|
+
|
|
112
|
+
`userTab`: `info` | `workspaces` | `datasets` | `reports` | `vesselGroups`.
|
|
113
|
+
|
|
114
|
+
<!-- Maintainers: every enumerated value above maps to a source symbol — see "Value sources" in ../MAINTENANCE.md for update checks. -->
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# GFW map routes
|
|
2
|
+
|
|
3
|
+
App served under basename `/map` (already included in the script's `path` output).
|
|
4
|
+
Source: `apps/fishing-map/routes/_app` (TanStack Router file routes).
|
|
5
|
+
|
|
6
|
+
| Route type | Path pattern | Path params | When to use |
|
|
7
|
+
| --------------------- | ------------------------------------------------------------ | --------------------- | ------------------------------------------------------------------------------------ |
|
|
8
|
+
| `workspace` | `/` or `/$category/$workspaceId` | category, workspaceId | Browse/compare layers on the map |
|
|
9
|
+
| `workspaces-list` | `/$category` | category | List curated workspaces of a category |
|
|
10
|
+
| `report` | `/$category/$workspaceId/report/$datasetId/$areaId` | + datasetId, areaId | Aggregated report over an area (EEZ/FAO/RFMO/MPA) |
|
|
11
|
+
| `report` (global) | `/$category/$workspaceId/report` | — (no datasetId/areaId) | Whole-world aggregated report; omit `datasetId`+`areaId`, category defaults to `reports` |
|
|
12
|
+
| `report` (saved) | `/report/$reportId` | reportId | Open a user-saved report |
|
|
13
|
+
| `vessel` | `/$category/$workspaceId/vessel/$vesselId` | + vesselId | Vessel profile (identity, track, events) |
|
|
14
|
+
| `vessel-search` | `/$category/$workspaceId/vessel-search` | — | Search vessels by name/MMSI/IMO or advanced filters |
|
|
15
|
+
| `vessel-group-report` | `/$category/$workspaceId/vessel-group-report/$vesselGroupId` | + vesselGroupId | Report over a vessel group |
|
|
16
|
+
| `ports-report` | `/$category/$workspaceId/ports-report/$portId` | + portId | Port activity profile (visits, vessels) |
|
|
17
|
+
| `user` | `/user` | — | User info, saved workspaces (`userTab=workspaces`), datasets, reports, vessel groups |
|
|
18
|
+
|
|
19
|
+
- `category`: `fishing-activity` (default) | `marine-manager` | `reports`
|
|
20
|
+
- `workspaceId`: `default-public` (default public workspace)
|
|
21
|
+
- report `datasetId` values: `public-eez-areas`, `public-fao-major`, `public-rfmo`, `public-mpa-all`
|
|
22
|
+
- `areaId`: numeric for EEZs (see `areas.json`), FAO major area code, RFMO id
|
|
23
|
+
- `vesselId`: GFW vessel id (uuid-like, from vessel search results)
|
|
24
|
+
- `portId`: e.g. `arg-camarones` (see `ports.json`)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PATH_BASENAME,
|
|
3
|
+
DEFAULT_WORKSPACE_CATEGORY,
|
|
4
|
+
DEFAULT_WORKSPACE_ID,
|
|
5
|
+
ROUTE_PATHS,
|
|
6
|
+
} from '@fishing-map/config'
|
|
7
|
+
|
|
8
|
+
export { DEFAULT_PATH_BASENAME as DEFAULT_BASENAME }
|
|
9
|
+
|
|
10
|
+
export type MapRouteType =
|
|
11
|
+
| 'workspace'
|
|
12
|
+
| 'workspaces-list'
|
|
13
|
+
| 'report'
|
|
14
|
+
| 'vessel'
|
|
15
|
+
| 'vessel-search'
|
|
16
|
+
| 'vessel-group-report'
|
|
17
|
+
| 'ports-report'
|
|
18
|
+
| 'user'
|
|
19
|
+
|
|
20
|
+
export type MapRouteParams = {
|
|
21
|
+
category?: string
|
|
22
|
+
workspaceId?: string
|
|
23
|
+
datasetId?: string
|
|
24
|
+
areaId?: string
|
|
25
|
+
reportId?: string
|
|
26
|
+
vesselId?: string
|
|
27
|
+
vesselGroupId?: string
|
|
28
|
+
portId?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type MapRoute = { type: MapRouteType } & MapRouteParams
|
|
32
|
+
|
|
33
|
+
export type RouteNavigation = {
|
|
34
|
+
to: string
|
|
35
|
+
params: Record<string, string>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const required = (route: MapRoute, param: keyof MapRouteParams): string => {
|
|
39
|
+
const value = route[param]
|
|
40
|
+
if (!value) {
|
|
41
|
+
throw new Error(`route.${param} is required for route type "${route.type}"`)
|
|
42
|
+
}
|
|
43
|
+
return value
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns the TanStack Router `to` pattern and `params` for a route,
|
|
48
|
+
* matching the file routes in apps/fishing-map/routes/_app
|
|
49
|
+
*/
|
|
50
|
+
export const getRouteNavigation = (route: MapRoute): RouteNavigation => {
|
|
51
|
+
const category = route.category || DEFAULT_WORKSPACE_CATEGORY
|
|
52
|
+
const workspaceId = route.workspaceId || DEFAULT_WORKSPACE_ID
|
|
53
|
+
const workspaceParams = { category, workspaceId }
|
|
54
|
+
switch (route.type) {
|
|
55
|
+
case 'workspace':
|
|
56
|
+
if (!route.category && !route.workspaceId) {
|
|
57
|
+
return { to: ROUTE_PATHS.HOME, params: {} }
|
|
58
|
+
}
|
|
59
|
+
return { to: ROUTE_PATHS.WORKSPACE, params: workspaceParams }
|
|
60
|
+
case 'workspaces-list':
|
|
61
|
+
return { to: ROUTE_PATHS.WORKSPACES_LIST, params: { category } }
|
|
62
|
+
case 'report':
|
|
63
|
+
if (route.reportId) {
|
|
64
|
+
return { to: ROUTE_PATHS.REPORT, params: { reportId: route.reportId } }
|
|
65
|
+
}
|
|
66
|
+
if (!route.datasetId && !route.areaId) {
|
|
67
|
+
return {
|
|
68
|
+
to: ROUTE_PATHS.WORKSPACE_REPORT,
|
|
69
|
+
params: { category: route.category || 'reports', workspaceId },
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
to: ROUTE_PATHS.WORKSPACE_REPORT_FULL,
|
|
74
|
+
params: {
|
|
75
|
+
...workspaceParams,
|
|
76
|
+
datasetId: required(route, 'datasetId'),
|
|
77
|
+
areaId: required(route, 'areaId'),
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
case 'vessel':
|
|
81
|
+
if (!route.category && !route.workspaceId) {
|
|
82
|
+
return { to: ROUTE_PATHS.VESSEL, params: { vesselId: required(route, 'vesselId') } }
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
to: ROUTE_PATHS.WORKSPACE_VESSEL,
|
|
86
|
+
params: { ...workspaceParams, vesselId: required(route, 'vesselId') },
|
|
87
|
+
}
|
|
88
|
+
case 'vessel-search':
|
|
89
|
+
return { to: ROUTE_PATHS.WORKSPACE_SEARCH, params: workspaceParams }
|
|
90
|
+
case 'vessel-group-report':
|
|
91
|
+
return {
|
|
92
|
+
to: ROUTE_PATHS.VESSEL_GROUP_REPORT,
|
|
93
|
+
params: { ...workspaceParams, vesselGroupId: required(route, 'vesselGroupId') },
|
|
94
|
+
}
|
|
95
|
+
case 'ports-report':
|
|
96
|
+
return {
|
|
97
|
+
to: ROUTE_PATHS.PORT_REPORT,
|
|
98
|
+
params: { ...workspaceParams, portId: required(route, 'portId') },
|
|
99
|
+
}
|
|
100
|
+
case 'user':
|
|
101
|
+
return { to: ROUTE_PATHS.USER, params: {} }
|
|
102
|
+
default:
|
|
103
|
+
throw new Error(`Unknown route type "${(route as MapRoute).type}"`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const buildRoutePath = (navigation: RouteNavigation): string => {
|
|
108
|
+
const path = navigation.to
|
|
109
|
+
.split('/')
|
|
110
|
+
.map((segment) =>
|
|
111
|
+
segment.startsWith('$')
|
|
112
|
+
? encodeURIComponent(navigation.params[segment.slice(1)] || '')
|
|
113
|
+
: segment
|
|
114
|
+
)
|
|
115
|
+
.join('/')
|
|
116
|
+
return path === '' ? '/' : path
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Static-segment patterns listed before parametric ones of the same length,
|
|
120
|
+
// so e.g. /user wins over /$category. Types map each ROUTE_PATHS pattern.
|
|
121
|
+
const ROUTE_PATTERNS: [string, MapRouteType][] = [
|
|
122
|
+
[ROUTE_PATHS.USER, 'user'],
|
|
123
|
+
[ROUTE_PATHS.SEARCH, 'vessel-search'],
|
|
124
|
+
[ROUTE_PATHS.REPORT, 'report'],
|
|
125
|
+
[ROUTE_PATHS.VESSEL, 'vessel'],
|
|
126
|
+
[ROUTE_PATHS.WORKSPACES_LIST, 'workspaces-list'],
|
|
127
|
+
[ROUTE_PATHS.WORKSPACE, 'workspace'],
|
|
128
|
+
[ROUTE_PATHS.WORKSPACE_SEARCH, 'vessel-search'],
|
|
129
|
+
[ROUTE_PATHS.WORKSPACE_VESSEL, 'vessel'],
|
|
130
|
+
[ROUTE_PATHS.WORKSPACE_REPORT, 'report'],
|
|
131
|
+
[ROUTE_PATHS.WORKSPACE_REPORT_FULL, 'report'],
|
|
132
|
+
[ROUTE_PATHS.VESSEL_GROUP_REPORT, 'vessel-group-report'],
|
|
133
|
+
[ROUTE_PATHS.PORT_REPORT, 'ports-report'],
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
const matchPattern = (pattern: string, segments: string[]): MapRouteParams | undefined => {
|
|
137
|
+
const patternSegments = pattern.split('/').filter(Boolean)
|
|
138
|
+
if (patternSegments.length !== segments.length) return undefined
|
|
139
|
+
const params: Record<string, string> = {}
|
|
140
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
141
|
+
const patternSegment = patternSegments[i]
|
|
142
|
+
if (patternSegment.startsWith('$')) {
|
|
143
|
+
params[patternSegment.slice(1)] = segments[i]
|
|
144
|
+
} else if (patternSegment !== segments[i]) {
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return params
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Matches a pathname (basename already stripped) against the ROUTE_PATHS patterns
|
|
153
|
+
*/
|
|
154
|
+
export const matchRoutePath = (pathname: string): MapRoute => {
|
|
155
|
+
const segments = pathname.split('/').filter(Boolean).map(decodeURIComponent)
|
|
156
|
+
if (segments.length === 0) {
|
|
157
|
+
return { type: 'workspace' }
|
|
158
|
+
}
|
|
159
|
+
for (const [pattern, type] of ROUTE_PATTERNS) {
|
|
160
|
+
const params = matchPattern(pattern, segments)
|
|
161
|
+
if (params) {
|
|
162
|
+
return { type, ...params }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { type: 'workspace', category: segments[0], workspaceId: segments[1] }
|
|
166
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import './register-gfw-resolver.mjs'
|
|
3
|
+
|
|
4
|
+
const readStdin = async () => {
|
|
5
|
+
let data = ''
|
|
6
|
+
for await (const chunk of process.stdin) data += chunk
|
|
7
|
+
return data
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const input = process.argv[2] && process.argv[2] !== '-' ? process.argv[2] : await readStdin()
|
|
11
|
+
if (!input?.trim()) {
|
|
12
|
+
console.error(
|
|
13
|
+
`Usage: node encode-url.mjs '{"route":{"type":"workspace"},"state":{...}}' (or pipe JSON via stdin)`
|
|
14
|
+
)
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { encodeMapUrl } = await import('@globalfishingwatch/skills/encode-url')
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = encodeMapUrl(JSON.parse(input))
|
|
22
|
+
console.log(JSON.stringify(result, null, 2))
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(error.message)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
@@ -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
|
+
})
|