Live bus locations from ACIS/OxonTime

I spent a day a little while back investigating how to get the raw data behind OxonTime’s live bus locations. Here’s how it works.

Please be aware that what follows is the result of a purely academic investigation, and that before using this information it may be worth contacting Oxfordshire County Council to discuss your plans, particularly if you’re going to distribute the results of your endeavours.

Performing a request

By monitoring the HTTP requests made by the applet we can deduce that it fetches data from a particular resource with parameters provided in the querystring. The resource is located at http://oxfordshire.acislive.com/pda/mainfeed.asp, and takes the following parameters:

type
One of STOPS, INIT or STATUS. The first retrieves a list of stops and their locations for a given area. The latter two both return a list of buses, with the latter being used for updates.
maplevel
I believe this only accepts the values 0 through 3, with nothing returned for 0 and 1. The results for 2 and 3 seem identical, so there seems little point in varying it.
SessionID
This seems to be a misnomer as it specifies the area you want to enquire about. We’ll explain how to convert to and from these numbers later.
systemid
Always 35 as it gets unhappy if you change it.
stopSelected
This expects an ATCO code yet seems to be ignored, so you may as well leave it as 34000000701.

vehicles
A comma-separated list of vehicle identifiers you currently believe to be in the area you’re enquiring about. This is only passed when type=STATUS, and lets you find out when the given buses have left the area.

Format of responses

The response in each case is a pipe-delimited list of values, with the first being the action the client should perform in updating its state. The things you may expect are:

STOP
Retrieves a list of stop locations in the area. Nearby stops are collected into one line.
NEW
This signifies that a bus is to be found at the given location.
DEL
These only appear when type=STATUS and signify that a bus is no longer at the location. If the bus is still within the requested area there will be a subsequent corresponding NEW action to give its new location. If it has left there will be no such NEW action. The buses that appear here will be a subset of those provided in the vehicles parameter of the querystring.

Now we know the form of the response, let’s see the values returned for each action. We’ll start with STOP.

STOP actions

A STOP action has the form:

STOP|35|naptan-codes|x,y|stop-names|stop-bearings|count

Here’s an example:

STOP|35|693123456^693234567^693345678|12,413|Banbury Road B1^Banbury Road B2^Banbury Road B3|172^179^340|3

As mentioned earlier, stops that are close to one another (e.g. on opposite sides of the road, or a ‘lettered’ group) are collected together, with count giving the number of stops at this location.

naptan-codes, stop-names and stop-bearings are each caret-delimited lists with fairly obvious contents. stop-bearings are given in clockwise degrees from grid north.

x and y give pixel offsets from the top-left corner of the displayed area. More on this later.

I have no idea what the 35 signifies, and currently assume it’s something to be with systemid.

A note on bus stop identifiers

The NaPTAN (National Public Transport Access Nodes) database provides two classes of identifiers, ATCO codes and NaPTAN codes. ATCO codes are upto 12 characters in length, whereas NaPTAN codes consist of nine digits. OxonTime predominantly exposes the latter; these are the numbers beginning ‘693′ displayed at bus stops. However, ATCO codes are used for the currentStop parameter and are accepted elsewhere in place of their equivalent NaPTAN codes.

The NaPTAN database is currently maintained under license from the Department for Transport by Thales. Access requires a license, which may come with a fee for commercial use. However, you may be interested to note that NaPTAN data is finding its way into OpenStreetMap.

NEW actions

These have the form:

NEW|identifier|orientation|service-name|Operators/common/bus/1|y,x

Here’s an example:

NEW|1024|4|X5|Operators/common/bus/1|45,302

identifier serves to keep track of buses between requests. I don’t know whether it has some further meaning outside of this API. orientation is an integer between 1 and 8 inclusive, being N, NE, E, SE, S, SW, W, NW respectively. service-name is the same as is used on the rest of the site (e.g. ‘S1′, ‘5′, ‘TUBE’). The next bit seems constant and can probably be safely ignored. Finally the offsets are given, only this time with y first; I have no idea why.

DEL actions

These have the form:

DEL|identifier|

Here’s an example:

DEL|1024|

These identifiers match up with those given in NEW actions. Note the trailing pipe.

By periodically making requests with type=STATUS one can process the returned lines of a stream of commands describing how to update the local state. This makes client implementation easier as you are effectively applying a diff, as opposed to having to compare new to old.

The co-ordinate system

First off, I’d better give you a disclaimer. This API and its associated co-ordinate system is very specific to the applet that is its only intended client. As such, the co-ordinate system provides exactly what it needs and no more.

Locations are addressable using a combination of SessionID — hereafter known as map number for clarity — and an x and y pixel offset from the top-left corner of that tile.


The maps are each 418 pixels square, and are arranged in a grid aligned with the British National Grid. The important thing to note about this is that grid North is not the same as true North, and that if you intend to plot these things on (for example) a Google or OpenLayers map, you’ll need to get your projections right.

The maps seem to be numbered somewhat arbitrarily, as shown by this map of bus stops and their associated map numbers. Colours are based on a hash of the number of the map they appear on.

A map showing the relative locations of bus stops and the ACIS map numbers for each map.

A map showing the relative locations of bus stops and the ACIS map numbers for each map.

These were found by requesting bus stops for all map numbers between 2500 and 3000, so are likely not complete. Predicting the map numbers for areas beyond these seems non-trivial or prone to error.

Doing the conversion

Edit: The following are for zoom-level 3 maps. Lower map numbers are at zoom-level 2 and cover nine times the area, so it probably makes more sense to retrieve and parse those.

To help you on your way, here is a Python dictionary which maps between map numbers and their top-left corners expressed as metres East and North of the SV square on the British National Grid. A pixel is equivalent to about 2.2×2.2 metres, as given by scale, making each map about 920 metres square.

map_numbers = {
    2507: (445998.30384769646, 204584.56186062991),
    2511: (446915.61022902746, 204584.49926297821),
    2512: (446913.71674095385, 205502.67433143588),
    2513: (446914.66775580525, 206418.97178315694),
    2516: (447831.1147245816, 205501.4561684193),
    2517: (447831.2934340348, 206419.14371744529),
    2520: (448749.2218840057, 205501.31610451243),
    2521: (448748.08603126393, 206418.72288576054),
    2522: (448749.21670514799, 207337.62523250855),
    2537: (448748.23059583915, 210084.58299463647),
    2538: (448747.09615575505, 211001.00119758685),
    2543: (450582.89109881676, 200918.25920371327),
    2545: (450582.58945117606, 202753.0387530663),
    2546: (450582.18354506971, 203668.85981883231),
    2548: (451497.38359153748, 201836.31023917606),
    2549: (451498.11202925985, 202752.26514351295),
    2550: (451499.38388189324, 203669.3077042877),
    2551: (452417.47236742743, 200918.93886234396),
    2552: (452416.98231942754, 201835.54957236422),
    2554: (452414.24108836311, 203668.59070562778),
    2557: (449664.20383959071, 206418.02172485131),
    2560: (450580.70403987489, 205501.67253490919),
    2561: (450580.96866136562, 206419.57452165778),
    2562: (450592.76460073108, 207336.75147627734),
    2563: (451499.11722530029, 204585.22058827255),
    2564: (451500.02145752736, 205501.68224598444),
    2565: (451499.00067467656, 206418.26238617001),
    2567: (452415.21798651741, 204584.90006837764),
    2568: (452415.5239759339, 205501.35499095405),
    2569: (452415.91032238252, 206418.74768511369),
    2570: (452415.68718924839, 207335.36782698822),
    2572: (449663.33610940503, 209167.13277178022),
    2573: (449665.39498988277, 210084.39421717002),
    2574: (449664.42323207163, 211001.82726484918),
    2575: (450582.29631623952, 208251.89590644254),
    2576: (450581.9053864188, 209169.67328096708),
    2577: (450583.29205249622, 210086.52104155198),
    2583: (452414.4291987372, 208251.72387988595),
    2584: (452415.09925185668, 209169.53539625759),
    2588: (453333.46479139361, 201834.4451865858),
    2589: (453331.95987896924, 202751.40410128961),
    2590: (453332.13594133727, 203668.53383136354),
    2593: (454247.50073518371, 202751.23971250441),
    2594: (454248.12170020386, 203668.51922434368),
    2597: (455165.54771764105, 202751.03839555787),
    2598: (455165.47211411892, 203669.19776463267),
    2603: (453331.21754538687, 204585.5174666637),
    2604: (453331.98750959791, 205502.05752572144),
    2605: (453331.24806580396, 206417.68972628826),
    2606: (453332.1148533299, 207336.05405913451),
    2607: (454249.16141248826, 204585.23652909664),
    2608: (454248.23316329095, 205502.58158632374),
    2609: (454248.31319587171, 206418.29606150393),
    2610: (454248.53242847562, 207334.54058771511),
    2612: (455166.59495257848, 205501.09410160859),
    2613: (455166.73990575952, 206417.83877819678),
    2614: (455163.93216277892, 207334.39865799341),
    2618: (456080.67203641078, 207334.0465145215),
    2619: (453333.25990631629, 208252.02178324395),
    2623: (454248.23726063699, 208251.90292474729),
    2627: (455165.28811999154, 208252.35041511542),
    2631: (456082.48076873791, 208252.31447046637),
    2733: (459748.48375305417, 189000.38567863638),
    2760: (462499.33169628482, 184416.6509955432),
    2761: (462498.71278643585, 185335.01518757801),
    2762: (463414.76846406935, 182586.06825647893),
    2769: (460666.45730665681, 189003.07510846868),
    2770: (461582.15014206048, 186251.43149620973),
    2772: (461583.17539895047, 188083.54467407736),
    2785: (464333.41855637298, 181667.60420131753),
    2795: (467079.15163364826, 179833.99098493406),
    2798: (464332.1937042338, 182585.51349055913),
    2844: (459748.72631959885, 191750.65753935973),
    2847: (456997.5453540723, 194500.33944191789),
    2848: (456999.14465004159, 195420.23667532994),
    2852: (457916.38047195785, 195419.96546529085),
    2854: (458831.49276305328, 193585.87604164047),
    2862: (457000.69281007705, 197252.53583802056),
    2878: (460665.66056860518, 189915.02235057726),
    2880: (460665.57930458471, 191750.16239531059),
    2882: (461581.82949289313, 189919.59644253476),
    2883: (461583.02792168478, 190835.35835896427),
    2884: (461581.33536609553, 191751.06046902022),
    2885: (461580.22909733956, 192669.40825026957),
    2887: (462497.66216840595, 190833.82408314376),
    2892: (463416.08698396623, 191750.39838604111),
    2993: (456998.56464994745, 207334.51643302356),
    2997: (457917.34275805362, 207333.4961082915)
}
scale = (2.2004998202088553, 2.1987468516915558)

These offsets were calculated by:

  • For each stop, finding the absolute position as given by the NaPTAN database
  • Instantiating two variables, ∑∆offset and ∑∆position, to null 2D-vectors
  • For each pair of stops appearing on the same map, adding the positive differences of their offsets within the map, and their absolute positions, to the respective variables
  • Dividing ∑∆offset by ∑∆position to give a conversion factor between pixels and metres (given above as scale)
  • For each map finding the average of each stop’s location minus its offset multiplied by the conversion factor (given above in map_numbers)

Here’s a bit of Python to convert between a map number and x and y offsets, and the WGS84 co-ordinate system. I’ve cheated a little in using Django’s GIS functionality which in turn uses the ctypes module to call functions from the GEOS library. If you’re not using Python or don’t want such a large dependency then you may wish to read the documentation linked from this page on the Ordnance Survey website.

from django.contrib.gis.geos import Point

def to_wgs84(map_number, rel_pos):
    """
    Takes an ACIS map number and a two-tuple specifying the offset on that
    map. Returns a Point object under the WGS84 projection.
    """
    corner = map_numbers[map_number]
    # Differing signs as we're applying a left-down offset to a left-up position
    pos = (
        corner[0] + rel_pos[0] * scale[0],
        corner[1] - rel_pos[1] * scale[1],
    )

    # 27700 is BNG; 4326 is WGS84
    return Point(pos, srid=27700).transform(4326, clone=True)

def from_wgs84(point):
    """
    Takes a Point object under any projection and returns a map number and
    two-tuple for that point. Raises ValueError if the point does not lie on
    any maps we know about.
    """
    # Make sure we're using the British National Grid
    pos = point.transform(27700, clone=True)
    for map_number, corner in map_numbers.items():
        rel = (
            (pos[0] - corner[0]) / scale[0],
            (corner[0] - pos[1]) / scale[1],
        )
        # This is the right map if it appears in the 418 pixel square to the
        # lower-right of the the corner.
        if 0 <= rel_pos[0] < 418 and 0 <= rel_pos[1] < 418:
            return map_number, rel_pos
    raise ValueError("Appears on unknown map")

Next steps

The terms of use for the OxonTime website forbid using it for other than personal non-commercial purposes, making it an abuse of the terms to use this data in some sort of mash-up. From my reading of the terms, however, there’s nothing to stop one writing and distributing a client that uses this API directly. If you’ve got the time and inclination, why not write an iPhone/Android/mobile-du-jour client application using all sorts of fancy geolocation and free mapping data?

You could even scrape the real-time information from http://www.oxontime.com/pip/stop.asp?naptan=naptan-code&textonly=1 (just be wary about non-well-formed HTML). Obviously, make yourself aware of the terms, and if in doubt, contact Oxfordshire Country Council. Be warned that this API, though likely stable, comes with no guarantee to that effect. Also, it seems a little slow at times, so be gentle and treat it with respect.


BRII Blue Pages

Creating Oxford University Blue Pages is one of the objectives of the BRII Project (see http://brii.bodleian.ox.ac.uk.) The Blue Pages will be a directory of expertise. Through them you will be able to search for research activities and experts in Oxford University. As I see it, it will be a sort of mega tool allowing you to see what is in Oxford’s Research Information Infrastructure from

Stakeholder Buy-in

On the 9th of June we hosted an Assembly. The topic was Stakeholder buy-in and we invited participants from other 6 JISC-funded projects and a representative from the JISC. (Find the programme here.) Our aim was to have a small but friendly gathering where we could exchange ideas, issues and problems related to stakeholder engagement. Basically what we asked presenters was to share their

Stakeholder Analysis Report

I have been busy these last weeks writing the BRII Stakeholder Analysis report. It is almost ready! I hope to have it finished by next week. Sally has already read it and she has made some suggestions. I am working on them now.This report has over 13 thousand words (35 pages), and it will probably reach 14 thousand as I will write an executive summary after I finish it. To carry out this study I

Developing the Bluepages

The BRII project is very much on track.I finished the Stakeholder and User needs Analysis. Will be ready for distribution soon.Ben has been working on the development of the foundations of the Research Information Infrastructure (RII). He has also created an online store for the vocabularies resulting from the BRII project. The vocab site has been given a single, central location in Oxford and

Stakeholder Analysis report is ready

The BRII Stakeholder Analysis report is ready and available in PDF format from here. Any comments about the report are welcome. (Click on comments below or email me.)

BluePages – User Tests 1

Anusha, Monica and I have finished a first (short) round of user tests. We got very interesting feedback from them which we will use to design a first online version of the BluePages. We will have a second round in maybe a month time.In the mean time I am going to give you a brief overview of the process we designed for the user tests.We recruited a small number of testers from within the Library

BRII Inside O.R.

This week I got a copy of the Inside O.R. magazine – November issue with a short article about BRII. I wrote this article after the OR51 conference (Operational Research) where I presented a paper called Making Sense of Research Activity Information in the Information Systems stream.Thanks to Rajan Anketell, Inside O.R. editor.Click here to download whole issue.

Blue Pages – User Tests 2

After 3 intense weeks Anusha, Monica and I have completed a second round of user tests. This time no mock ups but with the real thing, which of course is the Blue Pages v0.00 :) We did 15 tests with a selection of academics, students, administrators and one research facilitator. Again feedback has proved to be an eye opener, and is helping us on our design.Tests lasted an average of 45 minutes

BRII’s Entity Store

In our internal meetings we use the term Registry or Entity Registry to refer to the Research Information Infrastructure. Wanting to know a bit more about the meaning and technological features of such kind of store I asked Anusha. She gave me the following lecture:Cecilia: What is an Entity Registry and what is the difference with conventional stores?Anusha: Lets first list our entities, so we

Copyright © 2007 Institutional Innovation. All rights reserved.