open-vector-tile
Version:
This library reads/writes Open Vector Tiles
373 lines • 13 kB
JavaScript
/**
* Create shapes
*
* Used by Layer's and Feature's M-Values
* Must be an object of key values
* all keys will be the same, values will be different
* A layer's Shape defines what the properties look like for every Feature in that layer
* so we only have to store the properties and M-Value shape **once** per layer.
* @param cache - the cache where all data is stored in a column format
* @param shape - the shape object to encode
* @returns - The index of where the shape was stored in the cache
*/
export function encodeShape(cache, shape) {
// this will store a "shape" of numbers on how to rebuild the object
const shapeStore = [];
// encode the shape data
_encodeShape(shape, shapeStore, cache);
// return the index of the shape index
return cache.addColumnData(9 /* OColumnName.shapes */, shapeStore);
}
/**
* Encodes a shape object into a format suitable for storing in a cache.
* @param shape - the shape object to encode. Recursively encodes nested objects.
* @param shapeStore - list of key (string) indices. Also includes if the value is an object or not.
* @param cache - the cache where all data is stored in a column format.
*/
function _encodeShape(shape, shapeStore, cache) {
if (Array.isArray(shape)) {
shapeStore.push(0);
_encodeShape(shape[0], shapeStore, cache);
}
else if (typeof shape === 'object') {
const entries = Object.entries(shape);
shapeStore.push(encodeAttribute(1, entries.length));
for (const [key, value] of entries) {
// store key
shapeStore.push(cache.addColumnData(1 /* OColumnName.string */, key));
_encodeShape(value, shapeStore, cache);
}
}
else {
shapeStore.push(encodeAttribute(2, shapePrimitiveToColumnName(shape)));
}
}
/**
* @param shapeIndex - the index to the key indices and whether the value is an object or not
* @param cache - the cache where all data is stored in a column format
* @returns - The shape object
*/
export function decodeShape(shapeIndex, cache) {
const shapeStore = cache.getColumn(9 /* OColumnName.shapes */, shapeIndex);
// duplicate the array to avoid modifying the original
return _decodeShape(cache, [...shapeStore]);
}
/**
* @param cache - the cache where all data is stored in a column format
* @param shapeStore - the shape data encoded as an array
* @returns - The shape object
*/
function _decodeShape(cache, shapeStore) {
const attribute = decodeAttribute(shapeStore.shift() ?? 0);
if (attribute.type === 0) {
// Array
return [_decodeShape(cache, shapeStore)];
}
else if (attribute.type === 1) {
// Object
const length = attribute.countOrCol;
const obj = {};
for (let i = 0; i < length; i++) {
const keyIndex = shapeStore.shift() ?? 0;
const key = cache.getColumn(1 /* OColumnName.string */, keyIndex);
obj[key] = _decodeShape(cache, shapeStore);
}
return obj;
}
else {
// Primitive value
return columnNameToShapePrimitive(attribute.countOrCol);
}
}
/**
* @param value - the value to encode
* @param shape - the shape of the value
* @param cache - the cache where all data is stored in a column format
* @returns - The index of where the value was stored in the cache
*/
export function encodeValue(value, shape, cache) {
const valueStore = [];
_encodeValue(value, shape, valueStore, cache);
return cache.addColumnData(9 /* OColumnName.shapes */, valueStore);
}
/**
* @param value - the value to encode
* @param shape - the shape of the value
* @param valueStore - list of key (string) indices. Also includes if the value is an object or not
* @param cache - the cache where all data is stored in a column format
*/
function _encodeValue(value, shape, valueStore, cache) {
// we follow the rules of the shape
if (Array.isArray(shape)) {
value = value;
valueStore.push(value.length);
for (const v of value) {
_encodeValue(v, shape[0], valueStore, cache);
}
}
else if (typeof shape === 'object') {
const keys = Object.keys(shape);
value = value;
for (const key of keys) {
// key stored already by shape
_encodeValue(value?.[key], shape[key], valueStore, cache);
}
}
else {
if (shape === 'string') {
valueStore.push(cache.addColumnData(1 /* OColumnName.string */, value ?? ''));
}
else if (shape === 'u64') {
valueStore.push(cache.addNumber(value ?? 0, 2 /* OColumnName.unsigned */));
}
else if (shape === 'i64') {
valueStore.push(cache.addNumber(value ?? 0, 3 /* OColumnName.signed */));
}
else if (shape === 'f32') {
valueStore.push(cache.addNumber(value ?? 0, 4 /* OColumnName.float */));
}
else if (shape === 'f64') {
valueStore.push(cache.addNumber(value ?? 0, 5 /* OColumnName.double */));
}
else if (shape === 'bool') {
valueStore.push(cache.addNumber(value ? 1 : 0, 2 /* OColumnName.unsigned */));
}
}
}
/**
* @param valueIndex - the index of the encoded value in the cache
* @param shape - the shape of the value to decode
* @param cache - the cache where all data is stored in a column format
* @returns The decoded value
*/
export function decodeValue(valueIndex, shape, cache) {
const valueStore = cache.getColumn(9 /* OColumnName.shapes */, valueIndex);
// duplicate the array to avoid modifying the original
return _decodeValue([...valueStore], shape, cache);
}
/**
* @param valueStore - the encoded value data as an array
* @param shape - the shape of the value to decode
* @param cache - the cache where all data is stored in a column format
* @returns The decoded value
*/
function _decodeValue(valueStore, shape, cache) {
if (Array.isArray(shape)) {
const length = valueStore.shift() ?? 0;
const arr = [];
for (let i = 0; i < length; i++) {
arr.push(_decodeValue(valueStore, shape[0], cache));
}
return arr;
}
else if (typeof shape === 'object') {
const obj = {};
for (const key in shape) {
obj[key] = _decodeValue(valueStore, shape[key], cache);
}
return obj;
}
else {
if (shape === 'null')
return null;
const columnValue = valueStore.shift() ?? 0;
if (shape === 'string') {
return cache.getColumn(1 /* OColumnName.string */, columnValue);
}
else if (shape === 'bool') {
return cache.getColumn(2 /* OColumnName.unsigned */, columnValue) !== 0;
}
else if (shape === 'u64') {
return cache.getColumn(2 /* OColumnName.unsigned */, columnValue);
}
else if (shape === 'i64') {
return cache.getColumn(3 /* OColumnName.signed */, columnValue);
}
else if (shape === 'f32') {
return cache.getColumn(4 /* OColumnName.float */, columnValue);
}
else {
// f64
return cache.getColumn(5 /* OColumnName.double */, columnValue);
}
}
}
/**
* @param type - 0 is array, 1 is object, 2 is value
* @param countOrColname - the length of the object or array; if value, its the column name
* - can match columns for [0-4] (string, u64, i64, f32, f64),
* - or matches a type: 5 represents bool and null, 6 represents array, 7 represents object
* @returns - the encoded message
*/
function encodeAttribute(type, countOrColname) {
return (countOrColname << 2) + type;
}
/**
* @param num - the column and index encoded together
* @returns - the decoded message
*/
function decodeAttribute(num) {
return { type: (num & 0b11), countOrCol: num >> 2 };
}
/**
* @param type - the primitive shape to convert
* @returns - the column name corresponding to the shape primitive
*/
function shapePrimitiveToColumnName(type) {
if (type === 'string')
return 1 /* OColumnName.string */;
else if (type === 'u64')
return 2 /* OColumnName.unsigned */;
else if (type === 'i64')
return 3 /* OColumnName.signed */;
else if (type === 'f32')
return 4 /* OColumnName.float */;
else if (type === 'f64')
return 5 /* OColumnName.double */;
else if (type === 'bool')
return 6;
else
return 7;
}
/**
* @param columnName - the column name to convert
* @returns - the primitive shape corresponding to the column name
*/
function columnNameToShapePrimitive(columnName) {
if (columnName === 1 /* OColumnName.string */)
return 'string';
else if (columnName === 2 /* OColumnName.unsigned */)
return 'u64';
else if (columnName === 3 /* OColumnName.signed */)
return 'i64';
else if (columnName === 4 /* OColumnName.float */)
return 'f32';
else if (columnName === 5 /* OColumnName.double */)
return 'f64';
else if (columnName === 6)
return 'bool';
else
return 'null';
}
//? The Following are utility functions when the user doesn't pre-define the Properties/M-Value
//? Shapes to store:
/**
* @param data - the data to create the shape from
* @returns - the shape type we want to create based upon the data
*/
export function createShapeFromData(data) {
const shape = {};
for (const d of data) {
for (const k in d)
shape[k] = getShapesValueType(d[k]);
}
return shape;
}
/**
* Update/Mutate the shape from the data provided
* @param shape - the shape
* @param data - the data to update the shape
*/
export function updateShapeFromData(shape, data) {
for (const k in data)
shape[k] = getShapesValueType(data[k]);
}
/**
* @param value - conform to the rules of OValue
* @returns - the shape type
*/
function getShapesValueType(value) {
if (Array.isArray(value)) {
// If it's a number type and the first number is a u64 but the second is an i64 or f64?
// otherwise just return the first case in the array
const types = value.map(getShapesValueType);
return [validateTypes(types)];
}
else if (typeof value === 'object' && value !== null) {
return createShapeFromData([value]);
}
else {
return getPrimitiveType(value);
}
}
/**
* @param value - the primtive value to get the type from
* @returns - the primitive type from the value
*/
function getPrimitiveType(value) {
const type = typeof value;
if (type === 'string') {
return 'string';
}
else if (type === 'number') {
if (Number.isInteger(value)) {
return value < 0 ? 'i64' : 'u64';
}
else {
return 'f64';
}
}
else if (type === 'boolean') {
return 'bool';
}
else {
return 'null';
}
}
/**
* This is primarily to check if the type is a primitive.
* If the primitive is a number, find the "depth", the most complex is f64, then i64, then u64.
* Otherwise, if the primitives don't match, throw an error.
* If the type is NOT a primitive, ensure that all types in the array match
* @param types - either a primitive type, array, or object
* @returns - a single type from the list to validate the correct type to be parsed from values later
*/
export function validateTypes(types) {
if (typeof types[0] === 'string') {
// first ensure all primitive types are the same (excluding numbers)
let baseType = types[0];
const basePrim = isNumber(baseType);
for (const type of types) {
if (type !== baseType) {
if (isNumber(type) && basePrim) {
baseType = getHighestOrderNumber(baseType, type);
}
else {
throw new Error('All types must be the same');
}
}
}
return baseType;
}
else {
const typeCheck = types.every((t) => typeof t === typeof types[0] && Array.isArray(t) === Array.isArray(types[0]));
if (!typeCheck)
throw new Error('All types must be the same');
return types[0];
}
}
/**
* given a primitive type, check if one of them is a "number" type
* @param type - any primitive type
* @returns - true if the type is a number
*/
function isNumber(type) {
return type === 'i64' || type === 'u64' || type === 'f32' || type === 'f64';
}
/**
* @param typeA - either i64, u64, or f64
* @param typeB - either i64, u64, or f64
* @returns - the "highest order number" e.g. f64 > i64 > u64
*/
function getHighestOrderNumber(typeA, typeB) {
if (typeA === 'f64' || typeB === 'f64') {
return 'f64';
}
else if (typeA === 'i64' || typeB === 'i64') {
return 'i64';
}
else {
return 'u64';
}
}
//# sourceMappingURL=shape.js.map