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)) }