add protobuf encoding of style

This commit is contained in:
Konstantin Käfer
2014-01-08 12:10:46 +01:00
parent 528eaf475e
commit 4f8556a6b8
4 changed files with 640 additions and 0 deletions

140
bin/convert-style.js Executable file
View File

@@ -0,0 +1,140 @@
#!/usr/bin/env node
var style = require('./style.js');
var Protobuf = require('./protobuf.js');
var fs = require('fs');
var pbf = new Protobuf();
// enum
var bucket_type = {
fill: 1,
line: 2,
point: 3
};
// enum
var cap_type = {
round: 1,
}
// enum
var join_type = {
butt: 1,
bevel: 2
}
function createBucket(bucket, name) {
var pbf = new Protobuf();
pbf.writeTaggedString(1 /* name */, name);
pbf.writeTaggedVarint(2 /* type */, bucket_type[bucket.type]);
pbf.writeTaggedString(3 /* source_name */, bucket.datasource);
pbf.writeTaggedString(4 /* source_layer */, bucket.layer);
if (bucket.field) {
pbf.writeTaggedString(5 /* source_field */, bucket.field);
var values = Array.isArray(bucket.value) ? bucket.value : [bucket.value];
for (var i = 0; i < values.length; i++) {
pbf.writeTaggedString(6 /* source_values */, values[i]);
}
}
if (bucket.cap) {
pbf.writeTaggedVarint(7 /* cap */, cap_type[bucket.cap]);
}
if (bucket.join) {
pbf.writeTaggedVarint(8 /* join */, join_type[bucket.join]);
}
// TODO: font stuff
return pbf;
}
function createStructure(structure) {
var pbf = new Protobuf();
pbf.writeTaggedString(1 /* name */, structure.name);
if (structure.bucket) {
pbf.writeTaggedString(2 /* bucket_name */, structure.bucket);
} else if (structure.layers) {
for (var i = 0; i < structure.layers.length; i++) {
pbf.writeMessage(3 /* child_layers */, createStructure(structure.layers[i]));
}
}
return pbf;
}
function createWidth(width) {
var pbf = new Protobuf();
var values = [];
if (Array.isArray(width)) {
pbf.writeTaggedString(1 /* scaling */, width[0]);
for (var i = 1; i < width.length; i++) {
if (width[0] === 'stops') {
values.push(width[i].z, width[i].val);
} else {
values.push(width[i]);
}
}
} else {
values.push(width);
}
pbf.writePackedFloats(2 /* values */, values);
return pbf;
}
function createLayer(layer, name) {
var pbf = new Protobuf();
pbf.writeTaggedString(1 /* layer_name */, name);
if ('color' in layer) {
var color = layer.color.match(/^#([0-9a-f]{6})$/i);
if (!color) {
console.warn('invalid color');
} else {
pbf.writeTaggedUInt32(2 /* color */, parseInt(color[1] + 'ff', 16));
}
}
if ('antialias' in layer) {
pbf.writeTaggedBoolean(3 /* antialias */, layer.antialias);
}
if ('width' in layer) {
pbf.writeMessage(4 /* width */, createWidth(layer.width));
}
return pbf;
}
function createClass(klass) {
var pbf = new Protobuf();
pbf.writeTaggedString(1 /* name */, klass.name);
for (var name in klass.layers) {
pbf.writeMessage(2 /* layers */, createLayer(klass.layers[name], name));
}
return pbf;
}
for (var name in style.buckets) {
var bucket = style.buckets[name];
pbf.writeMessage(1 /* buckets */, createBucket(bucket, name));
}
for (var i = 0; i < style.structure.length; i++) {
var structure = style.structure[i];
pbf.writeMessage(2 /* structure */, createStructure(structure));
}
for (var i = 0; i < style.classes.length; i++) {
var klass = style.classes[i];
pbf.writeMessage(3 /* classes */, createClass(klass));
}
process.stdout.write(pbf.finish());

312
bin/protobuf.js Normal file
View File

@@ -0,0 +1,312 @@
module.exports = Protobuf;
function Protobuf(buf, pos) {
if (Buffer.isBuffer(buf)) {
this.buf = buf;
} else {
this.buf = new Buffer(buf || 128);
}
this.pos = pos || 0;
this.length = this.buf.length;
}
// === READING =================================================================
Protobuf.prototype.readUInt32 = function() {
var val = this.readUInt32LE(this.pos);
this.pos += 4;
return val;
};
Protobuf.prototype.readUInt64 = function() {
var val = this.readUInt64LE(this.pos);
this.pos += 4;
return val;
};
Protobuf.prototype.readVarint = function() {
// TODO: bounds checking
var pos = this.pos;
if (this.buf[pos] <= 0x7f) {
this.pos++;
return this.buf[pos];
} else if (this.buf[pos + 1] <= 0x7f) {
this.pos += 2;
return (this.buf[pos] & 0x7f) | (this.buf[pos + 1] << 7);
} else if (this.buf[pos + 2] <= 0x7f) {
this.pos += 3;
return (this.buf[pos] & 0x7f) | (this.buf[pos + 1] & 0x7f) << 7 | (this.buf[pos + 2]) << 14;
} else if (this.buf[pos + 3] <= 0x7f) {
this.pos += 4;
return (this.buf[pos] & 0x7f) | (this.buf[pos + 1] & 0x7f) << 7 | (this.buf[pos + 2] & 0x7f) << 14 | (this.buf[pos + 3]) << 21;
} else if (this.buf[pos + 4] <= 0x7f) {
this.pos += 5;
return ((this.buf[pos] & 0x7f) | (this.buf[pos + 1] & 0x7f) << 7 | (this.buf[pos + 2] & 0x7f) << 14 | (this.buf[pos + 3]) << 21) + (this.buf[pos + 4] * 268435456);
} else {
throw new Error("TODO: Handle 6+ byte varints");
}
};
Protobuf.prototype.readSVarint = function() {
var num = this.readVarint();
if (num > 2147483647) throw new Error('TODO: Handle numbers >= 2^30');
// zigzag encoding
return ((num >> 1) ^ -(num & 1));
};
Protobuf.prototype.readString = function() {
var bytes = this.readVarint();
// Node.js
var str = this.buf.toString('utf8', this.pos, this.pos + bytes);
// Client side JS code:
// // TODO: bounds checking
// var chr = String.fromCharCode;
// var b = this.buf;
// var p = this.pos;
// var end = this.pos;
// var str = '';
// while (p < end) {
// if (b[p] <= 0x7F) str += chr(b[p++]);
// else if (b[p] <= 0xBF) throw new Error('Invalid UTF-8 codepoint: ' + b[p]);
// else if (b[p] <= 0xDF) str += chr((b[p++] & 0x1F) << 6 | (b[p++] & 0x3F));
// else if (b[p] <= 0xEF) str += chr((b[p++] & 0x1F) << 12 | (b[p++] & 0x3F) << 6 | (b[p++] & 0x3F));
// else if (b[p] <= 0xF7) p += 4; // We can't handle these codepoints in JS, so skip.
// else if (b[p] <= 0xFB) p += 5;
// else if (b[p] <= 0xFD) p += 6;
// else throw new Error('Invalid UTF-8 codepoint: ' + b[p]);
// }
this.pos += bytes;
return str;
};
Protobuf.prototype.readPacked = function(type) {
// TODO: bounds checking
var bytes = this.readVarint();
var end = this.pos + bytes;
var array = [];
while (this.pos < end) {
array.push(this['read' + type]());
}
return array;
};
Protobuf.prototype.skip = function(val) {
// TODO: bounds checking
var type = val & 0x7;
switch (type) {
/* varint */ case 0: while (this.buf[this.pos++] > 0x7f); break;
/* 64 bit */ case 1: this.pos += 8; break;
/* length */ case 2: var bytes = this.readVarint(); this.pos += bytes; break;
/* 32 bit */ case 5: this.pos += 4; break;
default: throw new Error('Unimplemented type: ' + type);
}
};
// Generic read function
Protobuf.prototype.read = function(spec, end) {
var ref, tag;
var obj = {};
for (tag in spec) {
ref = spec[tag];
if ('default' in ref) {
obj[ref.name] = ref['default'];
}
if ('repeated' in ref) {
obj[ref.name] = [];
}
}
var begin = this.pos;
if (typeof end == 'undefined') {
var bytes = this.readVarint();
end = this.pos + bytes;
}
while (this.pos < end) {
var val = this.readVarint();
tag = val >> 3;
ref = spec[tag];
if (ref) {
if (ref.packed) {
obj[ref.name] = this.readPacked(ref.type);
}
else if (ref.repeated) {
if (!obj[ref.name]) obj[ref.name] = [];
obj[ref.name].push(this['read' + ref.type]());
} else {
obj[ref.name] = this['read' + ref.type]();
}
} else {
this.skip(val);
}
}
if (this._debug) {
obj._begin = begin;
obj._end = end;
obj._bytes = obj._end - obj._begin;
}
for (tag in spec) {
ref = spec[tag];
if (!(ref.name in obj) && ('required' in ref)) {
throw new Error('Field ' + ref.name + ' is required');
}
}
return obj;
};
// === WRITING =================================================================
Protobuf.Varint = 0;
Protobuf.Int64 = 1;
Protobuf.Message = 2;
Protobuf.String = 2;
Protobuf.Packed = 2;
Protobuf.Int32 = 5;
Protobuf.prototype.writeTag = function(tag, type) {
this.writeVarint((tag << 3) | type);
};
Protobuf.prototype.writeUInt32 = function(val) {
while (this.length < this.pos + 4) this.realloc();
this.buf.writeUInt32LE(val, this.pos);
this.pos += 4;
};
Protobuf.prototype.writeTaggedUInt32 = function(tag, val) {
this.writeTag(tag, Protobuf.Int32);
this.writeUInt32(val);
};
Protobuf.prototype.writeVarint = function(val) {
val = Number(val);
if (isNaN(val)) {
val = 0;
}
if (val <= 0x7f) {
while (this.length < this.pos + 1) this.realloc();
this.buf[this.pos++] = val;
} else if (val <= 0x3fff) {
while (this.length < this.pos + 2) this.realloc();
this.buf[this.pos++] = 0x80 | ((val >>> 0) & 0x7f);
this.buf[this.pos++] = 0x00 | ((val >>> 7) & 0x7f);
} else if (val <= 0x1ffffff) {
while (this.length < this.pos + 3) this.realloc();
this.buf[this.pos++] = 0x80 | ((val >>> 0) & 0x7f);
this.buf[this.pos++] = 0x80 | ((val >>> 7) & 0x7f);
this.buf[this.pos++] = 0x00 | ((val >>> 14) & 0x7f);
} else if (val <= 0xfffffff) {
while (this.length < this.pos + 4) this.realloc();
this.buf[this.pos++] = 0x80 | ((val >>> 0) & 0x7f);
this.buf[this.pos++] = 0x80 | ((val >>> 7) & 0x7f);
this.buf[this.pos++] = 0x80 | ((val >>> 14) & 0x7f);
this.buf[this.pos++] = 0x00 | ((val >>> 21) & 0x7f);
} else {
throw new Error("TODO: Handle 5+ byte varints (" + val + ")");
}
};
Protobuf.prototype.writeTaggedVarint = function(tag, val) {
this.writeTag(tag, Protobuf.Varint);
this.writeVarint(val);
};
Protobuf.prototype.writeBoolean = function(val) {
this.writeVarint(Boolean(val));
};
Protobuf.prototype.writeTaggedBoolean = function(tag, val) {
this.writeTaggedVarint(tag, Boolean(val));
};
Protobuf.prototype.writeString = function(str) {
str = String(str);
var bytes = Buffer.byteLength(str);
this.writeVarint(bytes);
while (this.length < this.pos + bytes) this.realloc();
this.buf.write(str, this.pos);
this.pos += bytes;
};
Protobuf.prototype.writeTaggedString = function(tag, str) {
this.writeTag(tag, Protobuf.String);
this.writeString(str);
};
Protobuf.prototype.writeFloat = function(val) {
while (this.length < this.pos + 4) this.realloc();
this.buf.writeFloatLE(val, this.pos);
this.pos += 4;
};
Protobuf.prototype.writeTaggedFloat = function(tag, val) {
this.writeTag(tag, Protobuf.Int32);
this.writeFloat(val);
};
Protobuf.prototype.writeDouble = function(val) {
while (this.length < this.pos + 8) this.realloc();
this.buf.writeDoubleLE(val, this.pos);
this.pos += 8;
};
Protobuf.prototype.writeTaggedDouble = function(tag, val) {
this.writeTag(tag, Protobuf.Int64);
this.writeDouble(val);
};
Protobuf.prototype.writeBuffer = function(buffer) {
var bytes = buffer.length;
this.writeVarint(bytes);
while (this.length < this.pos + bytes) this.realloc();
buffer.copy(this.buf, this.pos);
this.pos += bytes;
};
Protobuf.prototype.writeMessage = function(tag, protobuf) {
var buffer = protobuf.finish();
this.writeTag(tag, Protobuf.Message);
this.writeBuffer(buffer);
};
Protobuf.prototype.writeRepeated = function(type, tag, items) {
for (var i = 0; i < items.length; i++) {
this['write' + type](tag, items[i]);
}
};
Protobuf.prototype.writePacked = function(type, tag, items) {
if (!items.length) return;
var message = new Protobuf();
for (var i = 0; i < items.length; i++) {
message['write' + type](items[i]);
}
var data = message.finish();
this.writeTag(tag, Protobuf.Packed);
this.writeBuffer(data);
};
Protobuf.prototype.writePackedVarints = function(tag, items) {
this.writePacked('Varint', tag, items);
};
Protobuf.prototype.writePackedFloats = function(tag, items) {
this.writePacked('Float', tag, items);
};
Protobuf.prototype.realloc = function() {
var buf = new Buffer(this.buf.length * 2);
this.buf.copy(buf);
this.buf = buf;
this.length = this.buf.length;
};
Protobuf.prototype.finish = function() {
return this.buf.slice(0, this.pos);
};

127
bin/style.js Normal file
View File

@@ -0,0 +1,127 @@
"use strict";
module.exports = {
"buckets": {
"water": {
"datasource": "streets",
"layer": "water",
"type": "fill"
},
"road_large": {
"datasource": "streets",
"layer": "road", "field": "class", "value": ["motorway", "main"],
"type": "line", "cap": "round", "join": "bevel"
},
"road_regular": {
"datasource": "streets",
"layer": "road", "field": "class", "value": "street",
"type": "line", "cap": "round", "join": "bevel"
},
"road_limited": {
"datasource": "streets",
"layer": "road", "field": "class", "value": "street_limited",
"type": "line", "cap": "round", "join": "bevel"
},
"park": {
"datasource": "streets",
"layer": "landuse", "field": "class", "value": "park",
"type": "fill"
},
"wood": {
"datasource": "streets",
"layer": "landuse", "field": "class", "value": "wood",
"type": "fill"
},
"school": {
"datasource": "streets",
"layer": "landuse", "field": "class", "value": "school",
"type": "fill"
},
"cemetery": {
"datasource": "streets",
"layer": "landuse", "field": "class", "value": "cemetery",
"type": "fill"
},
"industrial": {
"datasource": "streets",
"layer": "landuse", "field": "class", "value": "industrial",
"type": "fill"
},
"building": {
"datasource": "streets",
"layer": "building",
"type": "fill"
},
"alcohol": {
"datasource": "streets",
"layer": "poi_label",
"field": "type",
"value": ["Alcohol"],
"type": "point"
}
},
"sprite": "img/maki-sprite",
"structure": [
{ "name": "park", "bucket": "park" },
{ "name": "wood", "bucket": "wood" },
{ "name": "water", "bucket": "water" },
{ "name": "road_limited", "bucket": "road_limited" },
{ "name": "road_regular", "bucket": "road_regular" },
{ "name": "road_large", "bucket": "road_large" },
{ "name": "alcohol", "bucket": "alcohol" }
],
"classes": [
{
"name": "default",
"layers": {
"background": {
"color": "#FFFFFF"
},
"park": {
"color": "#c8df9f",
"antialias": true
},
"wood": {
"color": "#33AA66",
"antialias": true
},
"water": {
"color": "#73b6e6",
"antialias": true
},
"road_limited": {
"color": "#BBBBBB",
"width": [
"stops",
{ z: 0, val: 1 },
{ z: 20, val: 1 }
]
},
"road_regular": {
"color": "#999999",
"width": [
"stops",
{ z: 0, val: 0.5 },
{ z: 13, val: 0.5 },
{ z: 16, val: 2 },
{ z: 20, val: 32 }
],
},
"road_large": {
"color": "#666666",
"width": [
"stops",
{ z: 0, val: 0.5 },
{ z: 11, val: 0.5 },
{ z: 13, val: 1 },
{ z: 16, val: 4 },
{ z: 20, val: 64 }
],
},
"alcohol": {
"image": "alcohol-shop"
}
}
}
]
};

61
proto/style.proto Normal file
View File

@@ -0,0 +1,61 @@
enum bucket_type {
fill = 1;
line = 2;
point = 3;
}
enum cap_type {
round = 1;
}
enum join_type {
butt = 1;
bevel = 2;
}
message bucket {
required string name = 1;
required bucket_type type = 2;
// Specify what data to pull into this bucket
required string source_name = 3;
required string source_layer = 4;
optional string source_field = 5;
repeated string source_values = 6;
// Specifies how the geometry for this bucket should be created
optional cap_type cap = 7;
optional join_type join = 8;
optional string font = 9;
optional float fontSize = 10;
}
message structure {
required string name = 1;
optional string bucket_name = 2;
repeated structure child_layers = 3;
}
message width {
optional string scaling = 1;
repeated float values = 2 [ packed = true ];
}
message layer {
required string layer_name = 1;
optional fixed32 color = 2; // rgba (=> rgb << 8 | 0xFF for opaque!)
optional bool antialias = 3;
optional width width = 4;
}
message class {
required string name = 1;
repeated layer layers = 2;
}
// root level object
message style {
repeated bucket buckets = 1;
repeated structure structure = 2;
repeated class classes = 3;
}