Task 499: .OV2 File Format
Task 499: .OV2 File Format
.OV2 File Format Specifications
The .OV2 file format is a binary format used by TomTom GPS devices to store Points of Interest (POI). It has no file header or magic number; it is simply a concatenation of variable-length records. All integers are signed 32-bit values stored in little-endian byte order. Longitude and latitude values are stored as integers representing degrees multiplied by 100,000 (e.g., -118.5 degrees is -11,850,000).
There are four record types:
- Type 0 (Deleted POI Record): Marks a deleted POI. Structure: type (1 byte = 0), record length (4 bytes), longitude (4 bytes), latitude (4 bytes), null-terminated string (optional, often empty).
- Type 1 (Skipper Record): Used for optimization to skip irrelevant sections. Fixed size of 21 bytes. Structure: type (1 byte = 1), block length (4 bytes, bytes to skip if outside bounding box), NE longitude (4 bytes), NE latitude (4 bytes), SW longitude (4 bytes), SW latitude (4 bytes).
- Type 2 (Regular POI Record): Standard POI. Structure: type (1 byte = 2), record length (4 bytes), longitude (4 bytes), latitude (4 bytes), null-terminated string (name).
- Type 3 (Extended POI Record): Extended POI with additional data. Structure: type (1 byte = 3), record length (4 bytes), longitude (4 bytes), latitude (4 bytes), one or more null-terminated strings (name and extra data).
The file is read by parsing records sequentially until the end.
- List of all properties of this file format intrinsic to its file system:
- Binary format with little-endian signed 32-bit integers.
- No file header or signature.
- Sequence of variable-length records without padding.
- Record type (1 byte, values 0-3).
- Record length (4 bytes, for types 0, 2, 3; includes type and length fields).
- Longitude (4 bytes, signed int32, degrees * 100,000; for types 0, 2, 3 and bounding box in type 1).
- Latitude (4 bytes, signed int32, degrees * 100,000; for types 0, 2, 3 and bounding box in type 1).
- Null-terminated string(s) (variable length; name for type 2, name + extra for type 3, optional for type 0).
- Block length (4 bytes, for type 1; bytes to skip).
- Northeast longitude (4 bytes, for type 1).
- Northeast latitude (4 bytes, for type 1).
- Southwest longitude (4 bytes, for type 1).
- Southwest latitude (4 bytes, for type 1).
- File extension .OV2 associated with TomTom POI databases.
- Typical file size depends on number of POIs; no fixed limit.
- Two direct download links for .OV2 files:
- https://www.vintageinn.co.uk/content/dam/mb/POI/vintageinn.ov2 (Vintage Inns POI for TomTom)
- https://www.millerandcarter.co.uk/content/dam/mb/POI/millerandcarter.ov2 (Miller & Carter POI for TomTom)
- Ghost blog embedded HTML JavaScript for drag and drop to dump properties:
Drag and drop .OV2 file here
- Python class for .OV2:
import struct
class OV2Handler:
def __init__(self, filepath=None):
self.filepath = filepath
self.records = []
if filepath:
self.read()
def read(self):
with open(self.filepath, 'rb') as f:
data = f.read()
offset = 0
while offset < len(data):
type_ = data[offset]
offset += 1
if type_ == 1:
block_length, ne_lon, ne_lat, sw_lon, sw_lat = struct.unpack_from('<5i', data, offset)
self.records.append({
'type': type_,
'block_length': block_length,
'ne_lon': ne_lon / 100000,
'ne_lat': ne_lat / 100000,
'sw_lon': sw_lon / 100000,
'sw_lat': sw_lat / 100000
})
offset += 20
elif type_ in (0, 2, 3):
length, lon, lat = struct.unpack_from('<3i', data, offset)
offset += 12
str_end = offset
strings = []
while str_end < offset + (length - 13):
str_start = str_end
while str_end < len(data) and data[str_end] != 0:
str_end += 1
strings.append(data[str_start:str_end].decode('utf-8'))
str_end += 1 # skip null
self.records.append({
'type': type_,
'length': length,
'lon': lon / 100000,
'lat': lat / 100000,
'strings': strings
})
offset += length - 13
else:
break
def print_properties(self):
for record in self.records:
print(record)
print('---')
def write(self, filepath=None):
if not filepath:
filepath = self.filepath or 'output.ov2'
with open(filepath, 'wb') as f:
for record in self.records:
type_ = record['type']
f.write(struct.pack('<B', type_))
if type_ == 1:
f.write(struct.pack('<5i', record['block_length'], int(record['ne_lon'] * 100000), int(record['ne_lat'] * 100000), int(record['sw_lon'] * 100000), int(record['sw_lat'] * 100000)))
elif type_ in (0, 2, 3):
strings_bytes = b''.join(s.encode('utf-8') + b'\0' for s in record['strings'])
length = 1 + 4 + 4 + 4 + len(strings_bytes)
f.write(struct.pack('<3i', length, int(record['lon'] * 100000), int(record['lat'] * 100000)))
f.write(strings_bytes)
- Java class for .OV2:
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class OV2Handler {
private String filepath;
private java.util.List<java.util.Map<String, Object>> records = new java.util.ArrayList<>();
public OV2Handler(String filepath) throws IOException {
this.filepath = filepath;
if (filepath != null) {
read();
}
}
public void read() throws IOException {
try (FileInputStream fis = new FileInputStream(filepath)) {
byte[] data = fis.readAllBytes();
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
int offset = 0;
while (offset < data.length) {
byte type = bb.get(offset);
offset += 1;
java.util.Map<String, Object> record = new java.util.HashMap<>();
record.put("type", (int) type);
if (type == 1) {
int blockLength = bb.getInt(offset);
offset += 4;
int neLon = bb.getInt(offset);
offset += 4;
int neLat = bb.getInt(offset);
offset += 4;
int swLon = bb.getInt(offset);
offset += 4;
int swLat = bb.getInt(offset);
offset += 4;
record.put("block_length", blockLength);
record.put("ne_lon", neLon / 100000.0);
record.put("ne_lat", neLat / 100000.0);
record.put("sw_lon", swLon / 100000.0);
record.put("sw_lat", swLat / 100000.0);
} else if (type == 0 || type == 2 || type == 3) {
int length = bb.getInt(offset);
offset += 4;
int lon = bb.getInt(offset);
offset += 4;
int lat = bb.getInt(offset);
offset += 4;
record.put("length", length);
record.put("lon", lon / 100000.0);
record.put("lat", lat / 100000.0);
int strOffset = offset;
java.util.List<String> strings = new java.util.ArrayList<>();
while (strOffset < offset + (length - 13)) {
int strStart = strOffset;
while (strOffset < data.length && data[strOffset] != 0) {
strOffset += 1;
}
strings.add(new String(data, strStart, strOffset - strStart, "UTF-8"));
strOffset += 1;
}
record.put("strings", strings);
offset += length - 13;
} else {
break;
}
records.add(record);
}
}
}
public void printProperties() {
for (java.util.Map<String, Object> record : records) {
System.out.println(record);
System.out.println("---");
}
}
public void write(String filepath) throws IOException {
try (FileOutputStream fos = new FileOutputStream(filepath == null ? this.filepath : filepath)) {
ByteBuffer bb = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN); // Buffer, resize if needed
for (java.util.Map<String, Object> record : records) {
int type = (int) record.get("type");
fos.write(type);
if (type == 1) {
bb.clear();
bb.putInt((int) record.get("block_length"));
bb.putInt((int) ((double) record.get("ne_lon") * 100000));
bb.putInt((int) ((double) record.get("ne_lat") * 100000));
bb.putInt((int) ((double) record.get("sw_lon") * 100000));
bb.putInt((int) ((double) record.get("sw_lat") * 100000));
fos.write(bb.array(), 0, 20);
} else if (type == 0 || type == 2 || type == 3) {
java.util.List<String> strings = (java.util.List<String>) record.get("strings");
byte[] stringsBytes = {};
for (String s : strings) {
byte[] sBytes = s.getBytes("UTF-8");
byte[] temp = new byte[stringsBytes.length + sBytes.length + 1];
System.arraycopy(stringsBytes, 0, temp, 0, stringsBytes.length);
System.arraycopy(sBytes, 0, temp, stringsBytes.length, sBytes.length);
temp[temp.length - 1] = 0;
stringsBytes = temp;
}
int length = 1 + 4 + 4 + 4 + stringsBytes.length;
bb.clear();
bb.putInt(length);
bb.putInt((int) ((double) record.get("lon") * 100000));
bb.putInt((int) ((double) record.get("lat") * 100000));
fos.write(bb.array(), 0, 12);
fos.write(stringsBytes);
}
}
}
}
}
- JavaScript class for .OV2:
class OV2Handler {
constructor(filepath = null) {
this.filepath = filepath;
this.records = [];
if (filepath) {
// For browser, use fetch or FileReader; here assume node with fs
const fs = require('fs');
this.read(fs.readFileSync(filepath));
}
}
read(data) {
const dataView = new DataView(data.buffer || data);
let offset = 0;
while (offset < data.byteLength) {
const type = dataView.getUint8(offset);
offset += 1;
let record = { type };
if (type === 1) {
record.block_length = dataView.getInt32(offset, true);
offset += 4;
record.ne_lon = dataView.getInt32(offset, true) / 100000;
offset += 4;
record.ne_lat = dataView.getInt32(offset, true) / 100000;
offset += 4;
record.sw_lon = dataView.getInt32(offset, true) / 100000;
offset += 4;
record.sw_lat = dataView.getInt32(offset, true) / 100000;
offset += 4;
} else if (type === 0 || type === 2 || type === 3) {
record.length = dataView.getInt32(offset, true);
offset += 4;
record.lon = dataView.getInt32(offset, true) / 100000;
offset += 4;
record.lat = dataView.getInt32(offset, true) / 100000;
offset += 4;
let strOffset = offset;
record.strings = [];
while (strOffset < offset + (record.length - 13)) {
let str = '';
let charCode = dataView.getUint8(strOffset);
while (charCode !== 0 && strOffset < data.byteLength) {
str += String.fromCharCode(charCode);
strOffset += 1;
charCode = dataView.getUint8(strOffset);
}
strOffset += 1;
if (str) record.strings.push(str);
}
offset += record.length - 13;
} else {
break;
}
this.records.push(record);
}
}
printProperties() {
this.records.forEach(record => {
console.log(record);
console.log('---');
});
}
write() {
// For node, write to file; here return buffer
let buffers = [];
this.records.forEach(record => {
let type = record.type;
buffers.push(new Uint8Array([type]).buffer);
if (type === 1) {
let dv = new DataView(new ArrayBuffer(20));
dv.setInt32(0, record.block_length, true);
dv.setInt32(4, Math.round(record.ne_lon * 100000), true);
dv.setInt32(8, Math.round(record.ne_lat * 100000), true);
dv.setInt32(12, Math.round(record.sw_lon * 100000), true);
dv.setInt32(16, Math.round(record.sw_lat * 100000), true);
buffers.push(dv.buffer);
} else if (type === 0 || type === 2 || type === 3) {
let stringsBytes = new Uint8Array(record.strings.reduce((acc, s) => acc.concat([...new TextEncoder().encode(s), 0]), []));
let length = 1 + 4 + 4 + 4 + stringsBytes.length;
let dv = new DataView(new ArrayBuffer(12));
dv.setInt32(0, length, true);
dv.setInt32(4, Math.round(record.lon * 100000), true);
dv.setInt32(8, Math.round(record.lat * 100000), true);
buffers.push(dv.buffer);
buffers.push(stringsBytes.buffer);
}
});
return new Blob(buffers).arrayBuffer(); // or write to file in node
}
}
- C class for .OV2:
In C, we use structs instead of classes, but here's a simple implementation using structs and functions.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef struct {
uint8_t type;
int32_t length; // for type 0,2,3
double lon;
double lat;
char** strings; // array of strings
int num_strings;
int32_t block_length; // for type 1
double ne_lon;
double ne_lat;
double sw_lon;
double sw_lat;
} OV2Record;
typedef struct {
char* filepath;
OV2Record* records;
int num_records;
} OV2Handler;
OV2Handler* ov2_create(const char* filepath) {
OV2Handler* handler = malloc(sizeof(OV2Handler));
handler->filepath = strdup(filepath);
handler->records = NULL;
handler->num_records = 0;
if (filepath) {
ov2_read(handler);
}
return handler;
}
void ov2_read(OV2Handler* handler) {
FILE* f = fopen(handler->filepath, "rb");
if (!f) return;
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t* data = malloc(size);
fread(data, 1, size, f);
fclose(f);
int offset = 0;
while (offset < size) {
uint8_t type = data[offset];
offset += 1;
handler->records = realloc(handler->records, (handler->num_records + 1) * sizeof(OV2Record));
OV2Record* rec = &handler->records[handler->num_records];
memset(rec, 0, sizeof(OV2Record));
rec->type = type;
if (type == 1) {
memcpy(&rec->block_length, data + offset, 4);
offset += 4;
int32_t ne_lon_int;
memcpy(&ne_lon_int, data + offset, 4);
rec->ne_lon = ne_lon_int / 100000.0;
offset += 4;
int32_t ne_lat_int;
memcpy(&ne_lat_int, data + offset, 4);
rec->ne_lat = ne_lat_int / 100000.0;
offset += 4;
int32_t sw_lon_int;
memcpy(&sw_lon_int, data + offset, 4);
rec->sw_lon = sw_lon_int / 100000.0;
offset += 4;
int32_t sw_lat_int;
memcpy(&sw_lat_int, data + offset, 4);
rec->sw_lat = sw_lat_int / 100000.0;
offset += 4;
} else if (type == 0 || type == 2 || type == 3) {
memcpy(&rec->length, data + offset, 4);
offset += 4;
int32_t lon_int;
memcpy(&lon_int, data + offset, 4);
rec->lon = lon_int / 100000.0;
offset += 4;
int32_t lat_int;
memcpy(&lat_int, data + offset, 4);
rec->lat = lat_int / 100000.0;
offset += 4;
int str_offset = offset;
rec->num_strings = 0;
rec->strings = NULL;
while (str_offset < offset + (rec->length - 13)) {
int str_start = str_offset;
while (str_offset < size && data[str_offset] != 0) str_offset += 1;
int str_len = str_offset - str_start;
rec->strings = realloc(rec->strings, (rec->num_strings + 1) * sizeof(char*));
rec->strings[rec->num_strings] = malloc(str_len + 1);
memcpy(rec->strings[rec->num_strings], data + str_start, str_len);
rec->strings[rec->num_strings][str_len] = '\0';
rec->num_strings += 1;
str_offset += 1;
}
offset += rec->length - 13;
} else {
break;
}
handler->num_records += 1;
}
free(data);
}
void ov2_print_properties(OV2Handler* handler) {
for (int i = 0; i < handler->num_records; i++) {
OV2Record rec = handler->records[i];
printf("Type: %d\n", rec.type);
if (rec.type == 1) {
printf("Block Length: %d\n", rec.block_length);
printf("NE Longitude: %f\n", rec.ne_lon);
printf("NE Latitude: %f\n", rec.ne_lat);
printf("SW Longitude: %f\n", rec.sw_lon);
printf("SW Latitude: %f\n", rec.sw_lat);
} else {
printf("Length: %d\n", rec.length);
printf("Longitude: %f\n", rec.lon);
printf("Latitude: %f\n", rec.lat);
printf("Strings: ");
for (int j = 0; j < rec.num_strings; j++) {
printf("%s ", rec.strings[j]);
}
printf("\n");
}
printf("---\n");
}
}
void ov2_write(OV2Handler* handler, const char* filepath) {
FILE* f = fopen(filepath ? filepath : handler->filepath, "wb");
if (!f) return;
for (int i = 0; i < handler->num_records; i++) {
OV2Record rec = handler->records[i];
fwrite(&rec.type, 1, 1, f);
if (rec.type == 1) {
fwrite(&rec.block_length, 4, 1, f);
int32_t ne_lon_int = (int32_t)(rec.ne_lon * 100000);
fwrite(&ne_lon_int, 4, 1, f);
int32_t ne_lat_int = (int32_t)(rec.ne_lat * 100000);
fwrite(&ne_lat_int, 4, 1, f);
int32_t sw_lon_int = (int32_t)(rec.sw_lon * 100000);
fwrite(&sw_lon_int, 4, 1, f);
int32_t sw_lat_int = (int32_t)(rec.sw_lat * 100000);
fwrite(&sw_lat_int, 4, 1, f);
} else {
int32_t length = rec.length;
fwrite(&length, 4, 1, f);
int32_t lon_int = (int32_t)(rec.lon * 100000);
fwrite(&lon_int, 4, 1, f);
int32_t lat_int = (int32_t)(rec.lat * 100000);
fwrite(&lat_int, 4, 1, f);
for (int j = 0; j < rec.num_strings; j++) {
fwrite(rec.strings[j], strlen(rec.strings[j]), 1, f);
uint8_t null_byte = 0;
fwrite(&null_byte, 1, 1, f);
}
}
}
fclose(f);
}
void ov2_destroy(OV2Handler* handler) {
for (int i = 0; i < handler->num_records; i++) {
for (int j = 0; j < handler->records[i].num_strings; j++) {
free(handler->records[i].strings[j]);
}
free(handler->records[i].strings);
}
free(handler->records);
free(handler->filepath);
free(handler);
}