aboutsummaryrefslogtreecommitdiff
path: root/src/serialization
diff options
context:
space:
mode:
Diffstat (limited to 'src/serialization')
-rw-r--r--src/serialization/utils.ts9
-rw-r--r--src/serialization/v0.ts122
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))
+}