diff options
Diffstat (limited to 'src/serialization')
-rw-r--r-- | src/serialization/utils.ts | 9 | ||||
-rw-r--r-- | src/serialization/v0.ts | 122 |
2 files changed, 131 insertions, 0 deletions
diff --git a/src/serialization/utils.ts b/src/serialization/utils.ts new file mode 100644 index 0000000..c94f199 --- /dev/null +++ b/src/serialization/utils.ts @@ -0,0 +1,9 @@ +import * as Base from 'lib/base' + +export function encodeNumber(n: bigint, length: number): string { + return Base.encode(n, Base.b2).padStart(length, '0') +} + +export function mod(a: number, b: number): number { + return ((a % b) + b) % b +} diff --git a/src/serialization/v0.ts b/src/serialization/v0.ts new file mode 100644 index 0000000..f90eb66 --- /dev/null +++ b/src/serialization/v0.ts @@ -0,0 +1,122 @@ +import * as Base from 'lib/base' +import * as Icons from 'lib/icons' +import * as State from 'state' +import * as Utils from 'serialization/utils' + +const posPrecision: number = 5 +const latLength: number = Base.encode(BigInt(`180${'0'.repeat(posPrecision)}`), Base.b2).length +const lngLength: number = Base.encode(BigInt(`360${'0'.repeat(posPrecision)}`), Base.b2).length +const colorLength: number = Base.encode(Base.decode('ffffff', Base.b16), Base.b2).length +const iconLength: number = 8 // At most 255 icons +const radiusLength: number = 5 + +// Encoding + +export function encode(s: State.State): string[] { + return s.map(encodeMarker) +} + +function encodeMarker({ pos, name, color, icon, radius }: State.Marker): string { + const lat = encodeLatOrLng(pos.lat, latLength, 180) // [-90; 90] + const lng = encodeLatOrLng(pos.lng, lngLength, 360) // [-180; 180] + return lat + lng + encodeColor(color) + encodeIcon(icon) + encodeRadius(radius) + encodeName(name) +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent +export const uriComponentBase: string[] = + '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.!~*\'()%'.split('') + +function encodeLatOrLng(n: number, length: number, range: number): string { + const [a, b] = Utils.mod(n + range / 2, range).toFixed(posPrecision).split('.') + return Utils.encodeNumber(BigInt(a + b), length) +} + +function encodeColor(color: string): string { + return Utils.encodeNumber(Base.decode(color.slice(1).toLowerCase(), Base.b16), colorLength) +} + +function encodeIcon(icon: string): string { + return icon == '' + ? '0' + : `1${Utils.encodeNumber(BigInt(Icons.keys().indexOf(icon) + 1), iconLength)}` +} + +function encodeRadius(radius: number): string { + if (radius == 0) { + return '0' + } else { + const binary = Base.encode(BigInt(radius), Base.b2) + const binaryLength = Utils.encodeNumber(BigInt(binary.length), radiusLength) + return `1${binaryLength}${binary}` + } +} + +function encodeName(str: string): string { + return str == '' + ? '' + : Base.encode(Base.decode(encodeURIComponent(str), uriComponentBase), Base.b2) +} + +// Decoding + +export function decode(encoded: string[]): State.State { + return encoded.map(binary => { + const [ lat, i1 ] = decodeLatOrLng(binary, 0, latLength, 180) + const [ lng, i2 ] = decodeLatOrLng(binary, i1, lngLength, 360) + const [ color, i3 ] = decodeColor(binary, i2) + const [ icon, i4 ] = decodeIcon(binary, i3) + const [ radius, i5 ] = decodeRadius(binary, i4) + const name = decodeName(binary, i5) + + return { + pos: { lat, lng }, + name, + color, + icon, + radius, + } + }) +} + +function decodeLatOrLng(encoded: string, i: number, length: number, range: number): [number, number] { + const slice = encoded.slice(i, i + length) + const digits = Base.decode(slice, Base.b2).toString() + const latOrLng = parseFloat(`${digits.slice(0, -posPrecision)}.${digits.slice(-posPrecision)}`) - range / 2 + return [ latOrLng, i + length ] +} + +function decodeColor(encoded: string, i: number): [ string, number ] { + const slice = encoded.slice(i, i + colorLength) + const color = `#${Base.encode(Base.decode(slice, Base.b2), Base.b16)}` + return [ color, i + colorLength ] +} + +function decodeIcon(encoded: string, i: number): [ string, number ] { + if (encoded.slice(i, i + 1) == '0') { + return [ '', i + 1 ] + } else { + const slice = encoded.slice(i + 1, i + 1 + iconLength) + const iconIndex = Number(Base.decode(slice, Base.b2)) - 1 + const icon = iconIndex < 0 ? '' : Icons.keys()[iconIndex] + return [ icon, i + 1 + iconLength ] + } +} + +function decodeRadius(encoded: string, i: number): [ number, number ] { + if (encoded.slice(i, i + 1) == '0') { + return [ 0, i + 1 ] + } else { + const binaryLength = encoded.slice(i + 1, i + 1 + radiusLength) + const length = Number(Base.decode(binaryLength, Base.b2)) + const binary = encoded.slice(i + 1 + radiusLength, i + 1 + radiusLength + length) + const radius = Number(Base.decode(binary, Base.b2)) + return [ radius, i + 1 + radiusLength + length ] + } +} + +function decodeName(encoded: string, i: number): string { + const slice = encoded.slice(i) + return slice == '' + ? '' + : decodeURIComponent(Base.encode(Base.decode(slice, Base.b2), uriComponentBase)) +} |