aboutsummaryrefslogtreecommitdiff
path: root/src/serialization/v0.ts
blob: f90eb661d4f36f5db4675c1deaf9dd3fe948f952 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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))
}