Skip to content

Commit 7db6717

Browse files
authored
feat: initial release of mapbox styles, NSR layer defs, and pin icon types (#1)
1 parent 3882517 commit 7db6717

13 files changed

Lines changed: 11514 additions & 4 deletions

README.md

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,89 @@
11
# @atb-as/mapbox-shared
22

3-
Mapbox style JSON, sprite icon naming, and National Stop Registry layer definitions shared between AtB's web planner and mobile app.
3+
Mapbox style JSON, sprite icon naming, and National Stop Registry (NSR) layer definitions shared between AtB's [travel planner web](https://github.com/AtB-AS/planner-web) and [mobile app](https://github.com/AtB-AS/mittatb-app).
44

5-
Initial contents landing via [the first PR](https://github.com/AtB-AS/mapbox-shared/pulls).
5+
## Install
6+
7+
```
8+
pnpm add @atb-as/mapbox-shared
9+
```
10+
11+
Requires `@atb-as/theme` at runtime for the `PinTheme` type (which is `'light' | 'dark'` derived from the theme package's `Themes<Theme>` map).
12+
13+
## What's in here
14+
15+
- `getMapboxLightStyle(userName, nsrTilesetId)` / `getMapboxDarkStyle(userName, nsrTilesetId)` — full Mapbox style JSONs, downloaded from Mapbox Studio and parameterized on the caller's user name + NSR tileset id.
16+
- `colorTheme` — the `color-theme` payload used with the Mapbox Standard basemap import (light + dark tuned for AtB).
17+
- `nsrSymbolLayers` / `nsrCircleLayers` — layer metadata for rendering stop places, quays, and parking from the NSR tileset (filter, zoom threshold, icon code, entity type).
18+
- `getLayerPropsDeterminedByZoomLevel(...)` — zoom-transition math for icon + label size/opacity.
19+
- Pin/icon naming types (`PinIcon`, `NsrPinIconCode`, `PinTheme`, etc.) matching the sprite conventions in [`AtB-AS/map-sprites`](https://github.com/AtB-AS/map-sprites).
20+
21+
Expression types use a strict operator union (`Expression = readonly [ExpressionName, ...]`) vendored from `@rnmapbox/maps`, which catches typos at the construction site here. The result is assignable both to `@rnmapbox/maps`'s types (identical) and to `mapbox-gl`'s looser `ExpressionSpecification` (a narrow type flows into a wider one) — no casts needed on either side.
22+
23+
## Updating the styles
24+
25+
We store Mapbox style JSON in this package rather than referencing it live from Mapbox Studio, because layer styling has to be expressed in code to support interactivity (Studio expressions are static), and Studio's asset workflow (SVG-only sprite uploads, buggy shadow rendering, duplicate uploads per OMS partner) doesn't fit AtB's setup. Sprites are hosted separately and injected by the consumer.
26+
27+
To pull an updated style from Studio:
28+
29+
1. Update the style in [Mapbox Studio](http://studio.mapbox.com/).
30+
2. Download it with the [Mapbox Styles API](https://docs.mapbox.com/api/maps/styles/#retrieve-a-style):
31+
```
32+
curl "https://api.mapbox.com/styles/v1/MAPBOX_USER_NAME/STYLE_URL_ID?access_token=PUBLIC_ACCESS_TOKEN"
33+
```
34+
3. Strip metadata and layers with `visibility: 'none'`:
35+
```js
36+
const style = /* paste curl response */;
37+
38+
const layers = style.layers
39+
.filter((l) => l?.layout?.visibility !== 'none')
40+
.map(({metadata, ...rest}) => rest);
41+
42+
copy({sources: style.sources, glyphs: style.glyphs, layers});
43+
```
44+
4. Paste into `src/mapbox-styles/get-mapbox-{light,dark}-style.ts`. Then:
45+
- Replace `sources.composite.url` and `glyphs` with the `${mapboxUserName}` / `${mapboxNsrTilesetId}` template literals used in the existing files.
46+
- Ensure there is no `sprite` field — consumers inject the sprite URL at runtime.
47+
- Update the "Based on" comment at the top of the file.
48+
- Test both light and dark modes. Watch out for cached tiles.
49+
50+
### Sprites
51+
52+
Icons are maintained in [`AtB-AS/map-sprites`](https://github.com/AtB-AS/map-sprites) and produce the following statically-hosted assets:
53+
54+
- `light.json`, `light.png`, `light@2x.json`, `light@2x.png`
55+
- `dark.json`, `dark.png`, `dark@2x.json`, `dark@2x.png`
56+
57+
Consumers configure the sprite URL at runtime (e.g. the mobile app reads it from firestore-configuration; the web planner has its own config).
58+
59+
## Local development
60+
61+
To test changes without cutting a release, use `pnpm add file:<path-to-mapbox-shared>` (e.g. `pnpm add file:../mapbox-shared`) in the project that depends on it.
62+
63+
Consumers import the built `lib/`, so run `pnpm build` when setting it up and whenever you make changes.
64+
65+
When you're done, set the package version in the consumer's `package.json` back to the previous version.
66+
67+
## Release
68+
69+
1. Merge a PR to main, where the commit message follows the [conventional commits specification](https://www.conventionalcommits.org/en/v1.0.0/).
70+
2. The Github action `release-please-action` will create a PR to update the package version and changelog.
71+
- `feat` will be a minor release.
72+
- `fix` will be a patch release.
73+
- Adding `!` after the prefix (e.g. `feat!`) means it is a breaking change, and will be a major release. This includes any changes to the public API that requires users of the package to update any code.
74+
- Other prefixes such as `chore` or `refactor` will not trigger a release.
75+
3. Merge the release PR to main to trigger a NPM release.
76+
77+
> [!NOTE]
78+
> In case you want to create a release with a different version number than the one suggested by release-please, you can make an empty commit on main with commit message on this format:
79+
> ```
80+
> chore: release v1.2.3
81+
>
82+
> release-as: 1.2.3
83+
> ```
84+
85+
For more details, see [release-please-action](https://github.com/googleapis/release-please-action).
86+
87+
## License
88+
89+
EUPL-1.2

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"private": false,
1212
"packageManager": "pnpm@11.1.1",
1313
"scripts": {
14-
"build": "tsc",
14+
"build": "tsc && cp src/mapbox-styles/mapbox-color-theme-data.json lib/mapbox-styles/",
1515
"lint": "eslint",
1616
"prettier": "prettier -c src",
1717
"check-all": "pnpm build && pnpm lint && pnpm prettier && pnpm test",

src/index.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,40 @@
1-
export {};
1+
// Mapbox style JSONs (verbatim from Mapbox Studio, minus vestigial layers
2+
// with no features in the current NSR tileset).
3+
export {getMapboxLightStyle} from './mapbox-styles/get-mapbox-light-style';
4+
export {getMapboxDarkStyle} from './mapbox-styles/get-mapbox-dark-style';
5+
6+
// Standard basemap color-theme (used inside style imports).
7+
export {colorTheme} from './mapbox-styles/mapbox-color-theme';
8+
9+
// Standardized calculations for icon size and opacity zoom transitions.
10+
export {getIconZoomTransitionStyle} from './mapbox-styles/get-icon-zoom-transition-style';
11+
12+
// Pin naming (used for sprite icon references like `stoppin_bus_default_light`).
13+
export type {
14+
NsrPinIconCode,
15+
VehicleIconCode,
16+
StationIconCode,
17+
PinScooterCompany,
18+
LiveVehiclePinState,
19+
PinType,
20+
PinIconCode,
21+
PinState,
22+
PinTheme,
23+
PinIcon,
24+
} from './mapbox-styles/pin-types';
25+
26+
// Filter helper for hiding the currently-selected feature from a layer.
27+
export {getFilterWhichAlsoHidesSelectedFeature} from './mapbox-styles/get-filter-which-also-hides-selected-feature';
28+
29+
// National Stop Registry (NSR) — layer definitions + zoom-transition math.
30+
export {
31+
nsrCircleLayers,
32+
nsrSymbolLayers,
33+
NsrSymbolLayerTextLocation,
34+
} from './national-stop-registry/nsr-layers';
35+
export type {NsrLayer} from './national-stop-registry/nsr-layers';
36+
export {
37+
getNsrLayerSourceProps,
38+
getLayerPropsDeterminedByZoomLevel,
39+
} from './national-stop-registry/nsr-utils';
40+
export type {LayerPropsDeterminedByZoomLevelParams} from './national-stop-registry/nsr-utils';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {FilterExpression} from '../types';
2+
3+
/**
4+
* Extends a layer filter with a condition that hides the currently selected
5+
* feature. Useful when the selected feature is rendered separately (e.g. in
6+
* its own layer with different styling), so it should be omitted from the
7+
* default layer to avoid rendering it twice.
8+
*
9+
* @param filter - existing filter, must be an array with 'all' as the first element
10+
* @param selectedFeaturePropertyId - id of the feature to hide, or undefined for none
11+
*/
12+
export const getFilterWhichAlsoHidesSelectedFeature = (
13+
filter: FilterExpression,
14+
selectedFeaturePropertyId: string | undefined,
15+
): FilterExpression => [
16+
...filter,
17+
['!=', ['get', 'id'], selectedFeaturePropertyId ?? ''],
18+
];
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {Expression} from '../types';
2+
3+
/*
4+
* Standardized calculations for icon size and opacity zoom transitions.
5+
*/
6+
export const getIconZoomTransitionStyle = (
7+
reachFullScaleAtZoomLevel: number,
8+
iconFullSize: number | Expression,
9+
scaleTransitionZoomRange: number,
10+
opacityTransitionExtraZoomRange: number,
11+
): {iconSize: Expression; iconOpacity: Expression} => {
12+
const iconOpacity: Expression = [
13+
'interpolate',
14+
['linear'],
15+
['zoom'],
16+
reachFullScaleAtZoomLevel - scaleTransitionZoomRange,
17+
0,
18+
reachFullScaleAtZoomLevel -
19+
scaleTransitionZoomRange +
20+
opacityTransitionExtraZoomRange,
21+
1,
22+
];
23+
24+
const iconSize: Expression = [
25+
'interpolate',
26+
['linear'],
27+
['zoom'],
28+
reachFullScaleAtZoomLevel - scaleTransitionZoomRange,
29+
0.3,
30+
reachFullScaleAtZoomLevel,
31+
iconFullSize,
32+
];
33+
34+
return {iconSize, iconOpacity};
35+
};

0 commit comments

Comments
 (0)