Custom Cities

Add new cities with custom maps and data

The cities API lets you add custom cities with their own maps, demand data, and buildings.

Register a City#

Use registerCity() to add a new city to the game.

javascript
window.SubwayBuilderAPI.registerCity({
    name: 'Montreal',
    code: 'MTL',
    description: 'Build metros beneath the Underground City',
    population: 4_300_000,
    initialViewState: {
        zoom: 13.5,
        latitude: 45.5017,
        longitude: -73.5673,
        bearing: 0
    },
    minZoom: 10,

    // Optional: Custom thumbnail for city select screen
    mapImageUrl: 'http://127.0.0.1:8080/MTL/thumbnail.svg'
});

City Properties#

PropertyTypeRequiredDescription
namestringYesDisplay name
codestringYesUnique code (uppercase, e.g., 'MTL')
descriptionstringNoCity description
populationnumberNoCity population
initialViewStateobjectYesStarting map position
initialViewState.zoomnumberYesInitial zoom level
initialViewState.latitudenumberYesInitial latitude
initialViewState.longitudenumberYesInitial longitude
initialViewState.bearingnumberNoInitial rotation (degrees)
minZoomnumberNoMinimum zoom level
mapImageUrlstringNoCustom thumbnail URL

Modded Cities Tab#

When you register custom cities, they appear in a dedicated "Modded" tab in the city selector. Modded cities are displayed with:

  • Purple-tinted styling to distinguish from built-in cities
  • A puzzle icon badge
  • "MOD" label if no population data is available
  • A count badge on the tab showing how many modded cities are available

Custom City Tabs#

Group multiple cities under a custom tab (e.g., for a "Canada" or "Europe" pack):

javascript
// First, register your cities
window.SubwayBuilderAPI.registerCity({
    name: 'Montreal',
    code: 'MTL'
    // ... city config
});

window.SubwayBuilderAPI.registerCity({
    name: 'Toronto',
    code: 'YYZ'
    // ... city config
});

// Then register a tab to group them
window.SubwayBuilderAPI.cities.registerTab({
    id: 'canada',
    label: 'Canada',
    emoji: 'πŸ‡¨πŸ‡¦',
    cityCodes: ['MTL', 'YYZ']
});

Tab properties:

PropertyRequiredDescription
idYesUnique identifier for the tab
labelYesDisplay name shown in the tab button
emojiNoEmoji shown next to the label (e.g., country flag)
cityCodesYesArray of city codes that belong to this tab

Custom tabs appear between the built-in country tabs (US, UK) and the "Modded" catch-all tab.

City Data Files#

Cities need data files to function properly. Set them using setCityDataFiles():

javascript
window.SubwayBuilderAPI.cities.setCityDataFiles('MTL', {
    buildingsIndex: '/data/MTL/buildings_index.json.gz',
    demandData: '/data/MTL/demand_data.json.gz',
    roads: '/data/MTL/roads.geojson.gz',
    runwaysTaxiways: '/data/MTL/runways_taxiways.geojson.gz',
    oceanDepthIndex: '/data/MTL/ocean_depth_index.json.gz' // Optional
});

Required data files:

FileDescription
demandDataPopulation demand points and commuter groups
buildingsIndexBuilding footprints and foundation depths
roadsRoad network for collision detection
runwaysTaxiwaysAirport areas (optional)
oceanDepthIndexOcean depth data (optional)

Data File Schemas#

The modding API exposes Zod schemas for validating your data files:

javascript
const schemas = window.SubwayBuilderAPI.schemas;

// Validate demand data
const demandData = { points: [...], pops: [...] };
const result = schemas.DemandDataSchema.safeParse(demandData);

if (result.success) {
    console.log('Demand data is valid!');
} else {
    console.error('Validation errors:', result.error.errors);
}

Demand Data Format#

javascript
{
    points: [
        {
            id: "dp_001",
            location: [-97.1463, 49.8718],  // [longitude, latitude]
            jobs: 500,
            residents: 1200,
            popIds: ["pop_001", "pop_002"]
        }
    ],
    pops: [
        {
            id: "pop_001",
            size: 100,                       // Number of commuters
            residenceId: "dp_001",          // Where they live
            jobId: "dp_002",                // Where they work
            drivingSeconds: 1800,           // Driving time in seconds
            drivingDistance: 15000,         // Distance in meters
            drivingPath: [                  // Optional: route geometry
                [-97.1463, 49.8718],
                [-97.1320, 49.8850]
            ]
        }
    ]
}

Available schemas:

  • DemandDataSchema - Complete demand file
  • DemandPointSchema - Single demand point
  • PopSchema - Single commuter group
  • BuildingIndexSchema - Building index
  • RoadsGeojsonSchema - Roads file
  • RunwaysTaxiwaysGeojsonSchema - Airports file

Custom Tiles#

For custom map tiles from localhost:

javascript
window.SubwayBuilderAPI.map.setTileURLOverride({
    cityCode: 'MTL',
    tilesUrl: 'http://127.0.0.1:8080/MTL/{z}/{x}/{y}.mvt',
    foundationTilesUrl: 'http://127.0.0.1:8080/MTL/{z}/{x}/{y}.mvt',
    maxZoom: 15
});

Layer Visibility#

Disable map layers by default for cities that don't have certain data:

javascript
window.SubwayBuilderAPI.map.setDefaultLayerVisibility('MTL', {
    buildingFoundations: false,
    oceanFoundations: false,
    trackElevations: false
});

Complete Example#

Here's a complete example adding Winnipeg:

javascript
// 1. Register city
window.SubwayBuilderAPI.registerCity({
    name: 'Winnipeg',
    code: 'YWG',
    description: 'Build a transit system for the Prairie metropolis',
    population: 850000,
    initialViewState: {
        zoom: 13.5,
        latitude: 49.871881,
        longitude: -97.146345,
        bearing: 0
    }
});

// 2. Point to localhost tiles
window.SubwayBuilderAPI.map.setTileURLOverride({
    cityCode: 'YWG',
    tilesUrl: 'http://127.0.0.1:8080/YWG/{z}/{x}/{y}.mvt',
    foundationTilesUrl: 'http://127.0.0.1:8080/YWG/{z}/{x}/{y}.mvt',
    maxZoom: 15
});

// 3. Set data files
window.SubwayBuilderAPI.cities.setCityDataFiles('YWG', {
    buildingsIndex: '/data/YWG/buildings_index.json.gz',
    demandData: '/data/YWG/demand_data.json.gz',
    roads: '/data/YWG/roads.geojson.gz',
    runwaysTaxiways: '/data/YWG/runways_taxiways.geojson.gz'
});

console.log('Winnipeg mod loaded!');

For more details on generating city data, see the Custom Cities Guide.