Metron’t - T&W Metro App Alternative
Started at the end of May 2025
What is it?⌗
- Platform: Android
- Language: Kotlin
- UI Toolkit: Jetpack Compose
A lot of people seem to think that the official Pop app for the Tyne and Wear Metro system isn’t as good as it could be. So, combining my special interest in trains with my programming skills, I decided to learn something new and make my own!
Just to be abundantly clear: this project is in no way affiliated, endorsed, or otherwise connected to Nexus. I am doing this purely as a hobby.
I started last month at the time of writing, and in that time I have developed a lot of new skills:
- The Kotlin programming language
- Android development practices
- General software development architectures
- Unidirectional data flow
- Separation of data layer and view layer
- Explored multiple map rendering libraries, including:
- osmdroid
- Google Maps
- MapLibre
- The Jetpack Compose library
- How REST APIs work and how to use Nexus’s API
- Material Design Guidelines
- How to turn parse KML and JSON data
Do note that it is far from done and that some icon assets are taken directly from Nexus currently. This is an issue that I would definitely sort out before releasing this as a download, if I ever get to that point.
Current capabilities⌗
- Displays a map of the Tyne and Wear Metro system, drawing each line on a MapLibre map.
- Allows you to search stations and pinpoint them on the map.
- Shows live locations of trains currently running, updated from Nexus’s API every ten seconds.
- Shows stations along the lines.
- Displays the zone, travel links, and car parks of selected station.
- Shows car park information of the stations that have them.
- Allows you to see information about a specific train on the tracks (WIP)
Tapping a station reveals a schedule window showing information for approaching trains:
- Destination
- Line colour
- Train colour
- Platform
- Due time
- An approximate description of location
This is not an embedded webview of the official Nexus metro map. It is a reimplementation using the MapLibre Native Android library. I had to use an AndroidView composable and some scope gymnastics in order to make that work.
@Composable
fun metroMapView(
stations: MutableMap<String, Station>,
trains: Map<String, Train>,
onShowBottomSheetChange: (Boolean) -> Unit,
onSelectedStationIdChange: (String) -> Unit,
onShowTrainSheetChange: (Boolean) -> Unit,
onSelectedTrainIdChange: (String) -> Unit
): MapView {
val context = LocalContext.current
MapLibre.getInstance(context)
val mapView: MapView by remember { mutableStateOf(MapView(context)) }
mapView.onCreate(null)
var trainSymbolManager: SymbolManager? by remember { mutableStateOf(null) }
AndroidView(
factory = { context ->
val mapStyle = Style.Builder().fromUri("https://tiles.openfreemap.org/styles/liberty")
mapStyle.withImage(
"station_icon",
ContextCompat.getDrawable(context, R.drawable.metro_smol)!!
)
mapStyle.withImage(
"train_icon",
ContextCompat.getDrawable(context, R.drawable.train_icon)!!
)
val metroLineSource = GeoJsonSource(
id = "metro_line_source",
geoJson = String(context.resources.openRawResource(R.raw.metromap2).readBytes()),
)
// Add lines ---------------------------------------------------------------------------
mapStyle.withSource(metroLineSource)
mapStyle.withLayer(
LineLayer("metro_lines_yellow", "metro_line_source")
.withProperties(
PropertyFactory.lineColor(context.getColor(R.color.metro_yellow)),
PropertyFactory.lineWidth(4f),
PropertyFactory.lineCap(LINE_CAP_ROUND)
)
.withFilter(eq(get("Line"), "Y"))
)
mapStyle.withLayer(
LineLayer("metro_lines_green", "metro_line_source")
.withProperties(
PropertyFactory.lineColor(context.getColor(R.color.metro_green)),
PropertyFactory.lineWidth(4f),
PropertyFactory.lineCap(LINE_CAP_ROUND)
)
.withFilter(eq(get("Line"), "G"))
)
// -------------------------------------------------------------------------------------
[...]
mapView
trainSymbolManager
is important as that allows me to alter the positions of the train as the API is pinged. This is done in the update
argument of AndroidView
:
update = {
if (trainSymbolManager == null) return@AndroidView
// Update trains
val unprocessedTrains = trains.values.toMutableSet()
// Iterate through all the icons on the map
for (symbol in trainSymbolManager!!.annotations.valueIterator()) {
// Get the train id associated with the symbol in its data
val symbolId = symbol.data?.asJsonObject["id"]?.asString?.removeSurrounding("\"")
// Remove associated train from the set, as it is being processed
unprocessedTrains.remove(trains[symbolId])
// If no train matching this icon found, remove from map.
if (!trains.contains(symbolId)) {
trainSymbolManager!!.delete(symbol)
continue
}
// If train still present, move to new location
val valueAnimator = ValueAnimator.ofObject(LatLngEvaluator(), symbol.latLng, trains[symbolId]?.latLng)
valueAnimator.duration = 1000
valueAnimator.interpolator = AccelerateDecelerateInterpolator()
valueAnimator.addUpdateListener { animation ->
symbol.latLng = animation.animatedValue as LatLng
trainSymbolManager!!.update(symbol)
}
valueAnimator.start()
}
// Create new icons for trains that are still in the set (new trains)
for (train in unprocessedTrains) {
trainSymbolManager!!.create(
SymbolOptions()
.withLatLng(train.latLng)
.withIconImage("train_icon")
.withData(JsonParser.parseString("{\"id\": \"${train.id}\"}"))
)
}
}
The other options on the navigation bar are currently placeholder. In fact, the car parks button doesn’t need to be there at all as that’s implemented in the map! I could maybe keep it there for ease of access later on, though that could risk some UI clutter as the same feature would be in different parts of the app.
The biggest stability risk to the app currently are undocumented API changes. Nexus’s API is not officially meant for public use, despite its API key being in plaintext on the Metro website, so any changes they make to the API could cause the app to crash. I am exploring ways to reduce the risk of this happening so that it can be at least somewhat usable in the real world outside my own personal use, though try-catch statements are realistically all I can do currently.
Credits⌗
- MapLibre, used under the BSD 2-Clause license.
- Nexus for their REST API service and icon assets. (Please let me know if you want me to use other assets!)
- danielgjackson for documenting Nexus’s undocumented API.
- solsensolsens for his vector reproduction of the official Nexus Metro Map’s train icon.
- National Rail for the National Rail logo.
- Google Fonts Icons, used under SIL Open Font Licence.