Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,89 @@
# @atb-as/mapbox-shared

Mapbox style JSON, sprite icon naming, and National Stop Registry layer definitions shared between AtB's web planner and mobile app.
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).

Initial contents landing via [the first PR](https://github.com/AtB-AS/mapbox-shared/pulls).
## Install

```
pnpm add @atb-as/mapbox-shared
```

Requires `@atb-as/theme` at runtime for the `PinTheme` type (which is `'light' | 'dark'` derived from the theme package's `Themes<Theme>` map).

## What's in here

- `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.
- `colorTheme` — the `color-theme` payload used with the Mapbox Standard basemap import (light + dark tuned for AtB).
- `nsrSymbolLayers` / `nsrCircleLayers` — layer metadata for rendering stop places, quays, and parking from the NSR tileset (filter, zoom threshold, icon code, entity type).
- `getLayerPropsDeterminedByZoomLevel(...)` — zoom-transition math for icon + label size/opacity.
- Pin/icon naming types (`PinIcon`, `NsrPinIconCode`, `PinTheme`, etc.) matching the sprite conventions in [`AtB-AS/map-sprites`](https://github.com/AtB-AS/map-sprites).

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.

## Updating the styles

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.

To pull an updated style from Studio:

1. Update the style in [Mapbox Studio](http://studio.mapbox.com/).
2. Download it with the [Mapbox Styles API](https://docs.mapbox.com/api/maps/styles/#retrieve-a-style):
```
curl "https://api.mapbox.com/styles/v1/MAPBOX_USER_NAME/STYLE_URL_ID?access_token=PUBLIC_ACCESS_TOKEN"
```
3. Strip metadata and layers with `visibility: 'none'`:
```js
const style = /* paste curl response */;

const layers = style.layers
.filter((l) => l?.layout?.visibility !== 'none')
.map(({metadata, ...rest}) => rest);

copy({sources: style.sources, glyphs: style.glyphs, layers});
```
4. Paste into `src/mapbox-styles/get-mapbox-{light,dark}-style.ts`. Then:
- Replace `sources.composite.url` and `glyphs` with the `${mapboxUserName}` / `${mapboxNsrTilesetId}` template literals used in the existing files.
- Ensure there is no `sprite` field — consumers inject the sprite URL at runtime.
- Update the "Based on" comment at the top of the file.
- Test both light and dark modes. Watch out for cached tiles.

### Sprites

Icons are maintained in [`AtB-AS/map-sprites`](https://github.com/AtB-AS/map-sprites) and produce the following statically-hosted assets:

- `light.json`, `light.png`, `[email protected]`, `[email protected]`
- `dark.json`, `dark.png`, `[email protected]`, `[email protected]`

Consumers configure the sprite URL at runtime (e.g. the mobile app reads it from firestore-configuration; the web planner has its own config).

## Local development

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.

Consumers import the built `lib/`, so run `pnpm build` when setting it up and whenever you make changes.

When you're done, set the package version in the consumer's `package.json` back to the previous version.

## Release

1. Merge a PR to main, where the commit message follows the [conventional commits specification](https://www.conventionalcommits.org/en/v1.0.0/).
2. The Github action `release-please-action` will create a PR to update the package version and changelog.
- `feat` will be a minor release.
- `fix` will be a patch release.
- 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.
- Other prefixes such as `chore` or `refactor` will not trigger a release.
3. Merge the release PR to main to trigger a NPM release.

> [!NOTE]
> 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:
> ```
> chore: release v1.2.3
>
> release-as: 1.2.3
> ```

For more details, see [release-please-action](https://github.com/googleapis/release-please-action).

## License

EUPL-1.2
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"private": false,
"packageManager": "[email protected]",
"scripts": {
"build": "tsc",
"build": "tsc && cp src/mapbox-styles/mapbox-color-theme-data.json lib/mapbox-styles/",
"lint": "eslint",
"prettier": "prettier -c src",
"check-all": "pnpm build && pnpm lint && pnpm prettier && pnpm test",
Expand Down
41 changes: 40 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,40 @@
export {};
// Mapbox style JSONs (verbatim from Mapbox Studio, minus vestigial layers
// with no features in the current NSR tileset).
export {getMapboxLightStyle} from './mapbox-styles/get-mapbox-light-style';
export {getMapboxDarkStyle} from './mapbox-styles/get-mapbox-dark-style';

// Standard basemap color-theme (used inside style imports).
export {colorTheme} from './mapbox-styles/mapbox-color-theme';

// Standardized calculations for icon size and opacity zoom transitions.
export {getIconZoomTransitionStyle} from './mapbox-styles/get-icon-zoom-transition-style';

// Pin naming (used for sprite icon references like `stoppin_bus_default_light`).
export type {
NsrPinIconCode,
VehicleIconCode,
StationIconCode,
PinScooterCompany,
LiveVehiclePinState,
PinType,
PinIconCode,
PinState,
PinTheme,
PinIcon,
} from './mapbox-styles/pin-types';

// Filter helper for hiding the currently-selected feature from a layer.
export {getFilterWhichAlsoHidesSelectedFeature} from './mapbox-styles/get-filter-which-also-hides-selected-feature';

// National Stop Registry (NSR) — layer definitions + zoom-transition math.
export {
nsrCircleLayers,
nsrSymbolLayers,
NsrSymbolLayerTextLocation,
} from './national-stop-registry/nsr-layers';
export type {NsrLayer} from './national-stop-registry/nsr-layers';
export {
getNsrLayerSourceProps,
getLayerPropsDeterminedByZoomLevel,
} from './national-stop-registry/nsr-utils';
export type {LayerPropsDeterminedByZoomLevelParams} from './national-stop-registry/nsr-utils';
18 changes: 18 additions & 0 deletions src/mapbox-styles/get-filter-which-also-hides-selected-feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {FilterExpression} from '../types';

/**
* Extends a layer filter with a condition that hides the currently selected
* feature. Useful when the selected feature is rendered separately (e.g. in
* its own layer with different styling), so it should be omitted from the
* default layer to avoid rendering it twice.
*
* @param filter - existing filter, must be an array with 'all' as the first element
* @param selectedFeaturePropertyId - id of the feature to hide, or undefined for none
*/
export const getFilterWhichAlsoHidesSelectedFeature = (
filter: FilterExpression,
selectedFeaturePropertyId: string | undefined,
): FilterExpression => [
...filter,
['!=', ['get', 'id'], selectedFeaturePropertyId ?? ''],
];
35 changes: 35 additions & 0 deletions src/mapbox-styles/get-icon-zoom-transition-style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Expression} from '../types';

/*
* Standardized calculations for icon size and opacity zoom transitions.
*/
export const getIconZoomTransitionStyle = (
reachFullScaleAtZoomLevel: number,
iconFullSize: number | Expression,
scaleTransitionZoomRange: number,
opacityTransitionExtraZoomRange: number,
): {iconSize: Expression; iconOpacity: Expression} => {
const iconOpacity: Expression = [
'interpolate',
['linear'],
['zoom'],
reachFullScaleAtZoomLevel - scaleTransitionZoomRange,
0,
reachFullScaleAtZoomLevel -
scaleTransitionZoomRange +
opacityTransitionExtraZoomRange,
1,
];

const iconSize: Expression = [
'interpolate',
['linear'],
['zoom'],
reachFullScaleAtZoomLevel - scaleTransitionZoomRange,
0.3,
reachFullScaleAtZoomLevel,
iconFullSize,
];

return {iconSize, iconOpacity};
};
Loading