> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/help/ja/llms.txt)

# Keep a tileset updated with Mapbox Tiling Service incremental updates

This tutorial covers how to create a [vector tileset](https://docs.mapbox.com/help/ja/help/glossary/tileset/#vector-tilesets) from third-party data using [Mapbox Tiling Service (MTS)](https://docs.mapbox.com/mapbox-tiling-service/guides/), and update it when the data changes using **incremental updates**.

Incremental updates allow you to efficiently update your tileset using only the parts of your data that have changed (creations, updates, or deletions), rather than uploading a full copy of the data and reprocessing the entire tileset. This is particularly useful for large datasets with small or frequent changes.

The tutorial uses real world data from the [Capital Bikeshare](https://www.capitalbikeshare.com/) system in Washington, DC, which provides real-time bike station data in the [General Bikeshare Feed Specification (GBFS)](https://gbfs.org/) format. This data feed includes details like the number of bikes and available docks at each station. You will create a tileset from this data, display it on a Mapbox GL JS map, and build a data pipeline to keep the data fresh with incremental updates.

### What you'll learn

-   Use Node.js to consume GBFS data and prepare it for upload to MTS
-   Create a tileset source, a tileset recipe, and publish a new tileset using the Tilesets CLI
-   Display the tileset with minimal styling and pop-ups in a Mapbox GL JS map
-   Create a script to detect changes in the data and generate a changeset for incremental updates

## Prerequisites

There are a few resources you'll need before getting started:

-   **Node.js and npm.** [Download and install](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) the latest version.
-   **Mapbox Access Tokens.** You will need a [secret access token](https://console.mapbox.com/account/access-tokens/create) with the scopes `tilesets:write`, `tilesets:read`, and `tilesets:list`. Do not share this token! You will also need a public access token to display the map.
-   **Tilesets CLI.** This command-line tool is used to interact with MTS. Follow the [installation instructions on GitHub](https://github.com/mapbox/tilesets-cli?tab=readme-ov-file#installation).
-   **Familiarity with vector tilesets and Mapbox GL JS vector sources.** This tutorial uses Mapbox GL JS to display your tileset on a map.
-   **A local web server.** This tutorial uses an HTML file to display a map. You can use any local web server. The [live-server plugin](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer) for Visual Studio Code is a good option, or you can use Python's built-in HTTP server with the command `python -m http.server` in the project directory.

Incremental updates are supported in version `1.14.0` or later of the [Tilesets CLI](https://github.com/mapbox/tilesets-cli). You can check your version with the command `tilesets --version`.

If you installed the Tilesets CLI using pip, you can upgrade it with the command:

```bash
$ pip install --upgrade mapbox-tilesets
```

> **Related content (tutorial): [Getting Started with MTS and the Tilesets CLI](https://docs.mapbox.com/help/ja/tutorials/get-started-mts-and-tilesets-cli/)**
> 
> This tutorial builds on concepts covered in the **Getting Started with MTS and the Tilesets CLI** tutorial. If you are new to MTS, it's recommended to start there first.

## Understanding the data feeds

This tutorial uses two GBFS (General Bikeshare Feed Specification) endpoints from the Capital Bikeshare system in Washington, DC. Each feed is publicly accessible. You can copy and paste the URLs below into your browser to inspect the JSON responses:

1.  **Station Information**, containing station location, names, and capacity:
    
    ```
    https://gbfs.lyft.com/gbfs/2.3/dca-cabi/en/station_information.json
    ```
    
    The response JSON includes an array of stations, each with properties like `station_id`, `name`, `lat`, `lon`, and `capacity`. This data rarely changes, but it provides the geographic context for each station.
    
    ```json
    {
         "data": {
             "stations": [
                 {
                     "region_id": "42",
                     "lon": -77.0352,
                     "rental_uris": {
                         "android": "https://dc.lft.to/lastmile_qr_scan",
                         "ios": "https://dc.lft.to/lastmile_qr_scan"
                     },
                     "station_id": "0825525a-1f3f-11e7-bf6b-3863bb334450",
                     "capacity": 15,
                     "lat": 38.92333,
                     "name": "15th & Euclid St NW",
                     "short_name": "31117"
                 },
                 ...
             ]    
         }
    }
    ```
    
2.  **Station Status**, containing real-time bike availability and dock status:
    
    ```
    https://gbfs.lyft.com/gbfs/2.3/dca-cabi/en/station_status.json
    ```
    
    The response JSON includes an array of stations with properties like `station_id`, `num_bikes_available`, and `num_docks_available`. This data is updated often throughout the day as bikes are checked in and out.
    
    ```json
    {
        "data": {
            "stations": [
                {
                    "is_installed": 1,
                    "num_ebikes_available": 0,
                    "num_bikes_disabled": 0,
                    "last_reported": 1752696562,
                    "num_bikes_available": 4,
                    "num_scooters_available": 0,
                    "num_docks_available": 7,
                    "vehicle_types_available": [
                    {
                        "vehicle_type_id": "1",
                        "count": 4
                    },
                    {
                        "vehicle_type_id": "2",
                        "count": 0
                    }
                    ],
                    "num_scooters_unavailable": 0,
                    "station_id": "1892760823152249488",
                    "num_docks_disabled": 0,
                    "is_returning": 1,
                    "is_renting": 1
                },
                ...
            ]
        }
    }
    ```
    

**Station information** rarely changes, but **station status** updates constantly throughout the day as bikes are checked in and out.

In the next step, you will add a Node.js script to download and process data from these feeds.

## Create GeoJSON from the bikeshare feeds

To make a vector tileset that can display the station's location, name, and bike availability, you must join the two GBFS feeds, combining each station's location coordinates and name with the real-time status data. Once, combined, each station will be a single GeoJSON point feature.

If you haven't already, create a new working directory for this project:

```bash
$ mkdir bikeshare-tileset
```

```bash
$ cd bikeshare-tileset
```

Create a Node script to fetch and combine the two feeds into a single GeoJSON file. This will be used as the source for your tileset.

Create `compile-bikeshare-stations.js` to fetch and combine both feeds. Once both feeds have been fetched, the script creates a GeoJSON feature for each station, using its longitude and latitude for the geometry, and including properties like station ID, name, capacity, and the number of bikes available.

To use incremental updates, each feature in the GeoJSON must have a unique ID that will not change between updates. This can either be an `id` property at the top level of the feature, any property in the `properties` object. You can also concatenate multiple properties to create a unique ID. In this tutorial, the `station_id` property is used as the unique ID for each feature.

```javascript
const fs = require('fs');

async function compileBikeshareStations() {
  // Fetch station info and status in parallel
  const [infoResponse, statusResponse] = await Promise.all([
    fetch('https://gbfs.lyft.com/gbfs/2.3/dca-cabi/en/station_information.json'),
    fetch('https://gbfs.lyft.com/gbfs/2.3/dca-cabi/en/station_status.json')
  ]);

  const stationInfo = (await infoResponse.json()).data.stations;
  const stationStatus = (await statusResponse.json()).data.stations;

  // Create status lookup map
  const statusMap = new Map(stationStatus.map(s => [s.station_id, s]));

  // Create GeoJSON features
  const features = stationInfo.map(station => {
    const status = statusMap.get(station.station_id);
    return {
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: [station.lon, station.lat]
      },
      properties: {
        id: station.station_id,
        name: station.name,
        capacity: station.capacity,
        num_bikes_available: status?.num_bikes_available || 0
      }
    };
  });

  // Write line-delimited GeoJSON
  const output = features.map(f => JSON.stringify(f)).join('\n') + '\n';
  fs.writeFileSync('data/bikeshare-stations.ld', output);

  console.log(`Wrote ${features.length} stations to data/bikeshare-stations.ld`);
}

compileBikeshareStations().catch(console.error);
```

Create the data directory and run `compile-bikeshare-stations.js` to fetch the data and create the GeoJSON file:

```bash
$ mkdir data
```

```bash
$ node compile-bikeshare-stations.js
```

This creates a [line-delimited GeoJSON](https://docs.mapbox.com/mapbox-tiling-service/vector/supported-file-formats/#line-delimited-geojson) file with ~800 bike stations, each containing station information and current bike availability.

Inspect `data/bikeshare-stations.ld` to see the format. Each line is a separate GeoJSON feature, containing the station's coordinates, name, capacity, and number of bikes available:

```json
{"type":"Feature","geometry":{"type":"Point","coordinates":[-77.0352,38.92333]},"properties":{"id":"0825525a-1f3f-11e7-bf6b-3863bb334450","name":"15th & Euclid St NW","capacity":15,"num_bikes_available":4}}
{"type":"Feature","geometry":{"type":"Point","coordinates":[-77.03138,38.9025]},"properties":{"id":"0825faf6-1f3f-11e7-bf6b-3863bb334450","name":"WAU / Flower Ave & Division St","capacity":15,"num_bikes_available":4}}
...
```

With this data prepared, you can now create a tileset source and recipe to generate a vector tileset.

## Create a tileset source, tileset recipe and publish the tileset

The next step uses the new line-delimited GeoJSON file to create a [**tileset source**](https://docs.mapbox.com/mapbox-tiling-service/guides/tileset-sources/), define a [**tileset recipe**](https://docs.mapbox.com/mapbox-tiling-service/guides/tileset-recipes/), and **publish** the tileset using the Tilesets CLI. These are routine steps for using Tilesets CLI to create and manage vector tilesets, so this tutorial will not cover them in detail. For more information, see the [Getting Started with MTS and the Tilesets CLI](https://docs.mapbox.com/help/ja/help/tutorials/get-started-mts-and-tilesets-cli/) tutorial.

These **Tilesets CLI** commands require a Mapbox secret access token with the scopes `tilesets:write`, `tilesets:read`, and `tilesets:list`. Set the `MAPBOX_ACCESS_TOKEN` environment variable to your secret access token before running these commands.

You can also append your access token to each command with the `--access-token` flag, but this requires more verbose syntax.

Learn how to set up environment variables for [Mac, Linux, or Windows](https://configu.com/blog/setting-env-variables-in-windows-linux-macos-beginners-guide/)

### Create a tileset source

```bash
$ tilesets upload-source {username} bikeshare-stations data/bikeshare-stations.ld
```

Replace `{username}` with your Mapbox username.

### Configure the tileset recipe

Create `recipe.json` to define how the tileset should be generated:

```json
{
  "version": 1,
  "incremental": true,
  "layers": {
    "stations": {
      "source": "mapbox://tileset-source/{username}/bikeshare-stations",
      "minzoom": 0,
      "maxzoom": 14,
      "features": {
        "id": ["get", "id"]
      }
    }
  }
}
```

Replace `{username}` with your Mapbox username.

### Create and publish the tileset

Create the tileset:

```bash
$ tilesets create {username}.bikeshare-stations --recipe recipe.json --name "Bikeshare Stations"
```

Publish it:

```bash
$ tilesets publish {username}.bikeshare-stations
```

Wait for the tileset to finish processing. You can check status with `tilesets status {username}.bikeshare-stations`, or log in to the [Data Manager](https://console.mapbox.com/studio/tilesets) in the Mapbox developer console.

Your new vector tileset is available for inclusion as a source in your Mapbox-powered maps using the tileset URL `mapbox://{username}.bikeshare-stations`, where `username` is your Mapbox username.

With your new tileset published, you can build a map to display the bikeshare data.

## Create the interactive map

In this step, you will consume the new vector tileset you created by adding it as a source in a Mapbox GL JS map. The map will display the bikeshare stations as circles, with the number of bikes available shown as labels.

Create `index.html` to display the tileset. Replace `YOUR_MAPBOX_ACCESS_TOKEN` with a public access token from your Mapbox account, and update the style JSON to include the tileset URL for your new tileset (replace `{username}` with your Mapbox username):

The JavaScript in this example loads a Mapbox GL JS map with a custom style JSON, importing the Mapbox Standard Style and adding the `bikeshare-stations` tileset as a vector source. The map displays the stations as a circle layer with a white fill and a dark stroke, and labels showing the number of bikes available at each station.

```html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Display a map on a webpage</title>
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
    <link href="https://api.mapbox.com/mapbox-gl-js/v3.25.0/mapbox-gl.css" rel="stylesheet">
    <script src="https://api.mapbox.com/mapbox-gl-js/v3.25.0/mapbox-gl.js"></script>
    <style>
        body {
            margin: 0;
            padding: 0;
        }

        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }
    </style>
</head>

<body>
    <div id="map"></div>
    <script>
        const map = new mapboxgl.Map({
            accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN'; // Replace with your public access token,
            style: {
                "version": 8,
                "imports": [{
                    "id": "basemap",
                    "url": "mapbox://styles/mapbox/standard"
                }],
                "sources": {
                    "stations": {
                        "type": "vector",
                        "url": "mapbox://{username}.bikeshare-stations"
                    }
                },
                "layers": [
                    {
                        "id": "stations",
                        "type": "circle",
                        "source": "stations",
                        "source-layer": "stations",
                        "paint": {
                            "circle-radius": 8,
                            "circle-color": "white",
                            "circle-stroke-color": "#333333",
                            "circle-stroke-width": 2
                        }
                    },
                    {
                        "id": "stations-labels",
                        "type": "symbol",
                        "source": "stations",
                        "source-layer": "stations",
                        "layout": {
                            "text-field": ["get", "num_bikes_available"],
                            "text-font": ["Open Sans Bold", "Arial Unicode MS Bold"],
                            "text-size": 10
                        },
                        "paint": {
                            "text-color": "#333333"
                        }
                    }
                ]
            },
            config: {
                basemap: {
                    theme: 'faded'
                }
            },
            container: 'map',
            center: [-77.03138, 38.9025],
            zoom: 11.2,
            hash: true
        });

        map.addInteraction('station-click', {
            type: 'click',
            target: {
                layerId: 'stations'
            },
            handler: (e) => {
                const coordinates = e.feature.geometry.coordinates.slice();
                const name = e.feature.properties.name;
                const numBikes = e.feature.properties.num_bikes_available;

                // Ensure that if the map is zoomed out such that multiple
                // copies of the feature are visible, the popup appears
                // over the copy being pointed to.
                while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
                    coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
                }

                new mapboxgl.Popup()
                    .setLngLat(coordinates)
                    .setHTML(`<strong>${name}</strong><br>Bikes available: ${numBikes}`)
                    .addTo(map);
            }
        });


        // Change the cursor to a pointer when hovering over stations
        map.addInteraction('station-mouseenter', {
            type: 'mouseenter',
            target: {
                layerId: 'stations'
            },
            handler: () => {
                map.getCanvas().style.cursor = 'pointer';
            }
        });

        map.addInteraction('station-mouseleave', {
            type: 'mouseleave',
            target: {
                layerId: 'stations'
            },
            handler: () => {
                map.getCanvas().style.cursor = '';
            }
        });

    </script>

</body>

</html>
```

Serve `index.html` with your local web server to see the bikeshare map with bike availability numbers displayed in each station circle.

![Interactive map showing bikeshare stations with bike availability numbers](https://docs.mapbox.com/help/ja/assets/ideal-img/tutorials--mts-incremental-updates--interactive-map.ccc7c35.480.png)

## Fetch new data and make a changeset

The map will now display the data, but the data will become stale as bikes are checked out and returned. To keep this information up to date, you can create a pipeline to update your tileset and keep the map fresh as the data changes.

Create `fetch-changes.js` to detect changes and generate changesets:

```javascript
const fs = require('fs');

async function fetchChanges() {
  const filepath = 'data/bikeshare-stations.geojson';
  
  if (!fs.existsSync(filepath)) {
    console.error(`Error: ${filepath} does not exist. Run compile-bikeshare-stations.js first.`);
    process.exit(1);
  }
  
  // Load existing data and fetch new status
  const existingData = fs.readFileSync(filepath, 'utf8');
  const existingFeatures = new Map(existingData.trim().split('\n')
    .map(line => JSON.parse(line))
    .map(f => [f.properties.id, f]));
  
  const statusResponse = await fetch('https://gbfs.lyft.com/gbfs/2.3/dca-cabi/en/station_status.json');
  const statusMap = new Map((await statusResponse.json()).data.stations
    .map(s => [s.station_id, s.num_bikes_available]));
  
  // Find changes and update features
  const changes = [];
  const updatedFeatures = [];
  
  for (const [id, feature] of existingFeatures) {
    const newBikeCount = statusMap.get(id) ?? feature.properties.num_bikes_available;
    const updatedFeature = {
      ...feature,
      properties: { ...feature.properties, num_bikes_available: newBikeCount }
    };
    
    if (newBikeCount !== feature.properties.num_bikes_available) {
      changes.push(JSON.stringify({ ...updatedFeature, _action: 'update' }));
    }
    
    updatedFeatures.push(updatedFeature);
  }
  
  if (changes.length > 0) {
    const changesetPath = `data/changeset-${Date.now()}.geojson`;
    fs.writeFileSync(changesetPath, changes.join('\n') + '\n');
    fs.writeFileSync(filepath, updatedFeatures.map(f => JSON.stringify(f)).join('\n') + '\n');
    
    console.log(`Generated changeset with ${changes.length} updates saved to ${changesetPath}`);
  } else {
    console.log('No changes detected');
  }
}

fetchChanges().catch(console.error);
```

It is important to remember that `fetch-changes.js` cannot compare the current bikeshare feed data with the tileset you published in the previous step. You must have a local source of data that matches the data in the tileset. In this case, the `data/bikeshare-stations.geojson` file is used as a source of truth for the bikeshare system data that is live in your tileset.

`fetch-changes.js` will generate a changeset file, and will also update `data/bikeshare-stations.geojson` so it matches the state of the bikeshare feed response.

Run `fetch-changes.js` to generate a changeset and update `data/bikeshare-stations.geojson`:

```bash
$ node fetch-changes.js
```

This generates a changeset file with a timestamp in the filename (e.g., `data/changeset-1234567890.geojson`) containing only the stations that changed, with each feature marked with `_action: 'update'`. This changeset is ready to be uploaded to MTS for incremental updates.

```json
{"type":"Feature","geometry":{"type":"Point","coordinates":[-77.08865851163864,38.84065400672152]},"properties":{"id":"0825e4d5-1f3f-11e7-bf6b-3863bb334450","name":"S Randolph St & Campbell Ave","capacity":15,"num_bikes_available":1},"_action":"update"}
{"type":"Feature","geometry":{"type":"Point","coordinates":[-76.997226,38.938889]},"properties":{"id":"8bbafd08-edb1-426d-94f4-c7b7d9be34dd","name":"John McCormack Rd NE","capacity":19,"num_bikes_available":2},"_action":"update"}
{"type":"Feature","geometry":{"type":"Point","coordinates":[-77.036278,38.912652]},"properties":{"id":"0826183f-1f3f-11e7-bf6b-3863bb334450","name":"16th & R St NW","capacity":19,"num_bikes_available":2},"_action":"update"}
...
```

## Upload the changeset

Upload the changeset to MTS using the `tilesets upload-changeset` command.

```bash
$ tilesets upload-changeset {username} bikeshare-stations-changeset data/changeset-1234567890.geojson --replace
```

You must provide your username, a name for the changeset, a path to the changeset file, and the `--replace` flag to make sure that this changeset replaces any previous changesets associated with this changeset name.

Your console will show the upload progress and return a JSON object with the changeset ID and size:

```bash
$ upload progress  [------------------------------------------------------]    0%
{"id": "mapbox://tileset-changeset/{username}/bikeshare-stations-changeset", "files": 1, "changesets_size": 43251, "file_size": 43251}
```

Your changeset is now uploaded, but will not affect the tileset until you publish it.

## Publish the changeset

Now that you have defined a changeset, you can refer to it to process and publish a new version of the tileset.

Like the recipe JSON used to create the tileset, you must provide a **changeset payload** JSON file to specify which source layers to apply the changeset to. Create `bikeshare-stations-changeset.json`:

```json
{
  "layers": {
    "stations": {
      "changeset": "mapbox://tileset-changeset/{username}/bikeshare-stations-changeset"
    }
  }
}
```

This file tells MTS to apply the changeset named `bikeshare-stations-changeset` to the `stations` layer of the `bikeshare-stations` tileset.

Publish the changeset with the `tilesets publish-changesets` command:

```bash
$ tilesets publish-changesets {username}.bikeshare-stations bikeshare-stations-changeset.json
```

Check the status of the publish job using `tilesets status`, or login to the [Data Manager](https://console.mapbox.com/studio/tilesets) in the Mapbox developer console.

```bash
$ tilesets status {username}.bikeshare-stations
```

## See the updates in your map

Wait for the publish job to complete, then refresh the map in the browser. The updated bike availability numbers will appear!

You may want to inspect a specific station from the changeset to confirm the update. Click on a station circle to see the popup with the updated number of bikes available.

Changes to tilesets, including incremental updates, may not appear immediately in the browser due to caching. If you don't see the changes right away, try clearing your browser cache or using a private/incognito window to bypass the cache.

You can repeat the process of running `fetch-changes.js` to detect changes, generating a new changeset, and publishing it to keep the tileset up to date with the latest bikeshare data as often as needed. Try it a few more times to see how the map updates with new data.

## Conclusion

Congratulations! You have successfully created a vector tileset from real-time bikeshare data, displayed it on a Mapbox GL JS map, and set up a pipeline to keep the data fresh with incremental updates.

### What we covered

-   Use Node.js to consume GBFS data and prepare it for upload to MTS
-   Create a tileset source, a tileset recipe, and publish a tileset using the Tilesets CLI
-   Display the tileset with minimal styling and pop-ups in a Mapbox GL JS map
-   Create a script to detect changes in the data and generate a changeset for incremental updates

### Next steps

Here are some things to try to expand on this data pipeline and the station map you created:

-   Improve the map styling to show a more complex marker, including both bikes and docks available.
-   Add more data to the tileset, such as station opening hours or bike types.
-   Automate the regular updates by using a cron job or a serverless function to run `fetch-changes.js` periodically.
-   Explore other data sources that provide real-time updates, such as public transit or weather data, and create similar tilesets.
-   Use your tileset to craft a custom style in [Mapbox Studio](https://studio.mapbox.com/), adding more layers and visualizations to enhance the map experience.